This commit is contained in:
davmar
2026-08-28 18:23:56 +02:00
commit 0311b8955a
50 changed files with 3120 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
/// Validated form that inserts a new [Item] into [ItemController].
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/item.dart';
import '../providers/item_provider.dart';
import '../widgets/app_text_field.dart';
import '../widgets/responsive_body.dart';
class AddItemScreen extends StatefulWidget {
const AddItemScreen({super.key});
@override
State<AddItemScreen> createState() => _AddItemScreenState();
}
class _AddItemScreenState extends State<AddItemScreen> {
final _formKey = GlobalKey<FormState>();
final _title = TextEditingController();
final _subtitle = TextEditingController();
final _description = TextEditingController();
ItemCategory? _category;
bool _submitted = false;
@override
void dispose() {
_title.dispose();
_subtitle.dispose();
_description.dispose();
super.dispose();
}
String? _requiredLength(String? value, {required int min, String? label}) {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Enter a ${label ?? 'value'}.';
if (text.length < min) {
return '${label ?? 'This field'} must be at least $min characters.';
}
return null;
}
void _submit() {
setState(() => _submitted = true);
final form = _formKey.currentState;
if (form == null || !form.validate() || _category == null) return;
context.read<ItemController>().addItem(
title: _title.text,
subtitle: _subtitle.text,
description: _description.text,
category: _category!,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Added “${_title.text.trim()}')),
);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Add item')),
body: ResponsiveBody(
child: Form(
key: _formKey,
autovalidateMode: _submitted
? AutovalidateMode.onUserInteraction
: AutovalidateMode.disabled,
child: ListView(
children: [
const SizedBox(height: 8),
Text(
'Create a catalog entry. All fields are required.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 20),
AppTextField(
controller: _title,
label: 'Title',
hint: 'Give it a short name',
autofocus: true,
textInputAction: TextInputAction.next,
validator: (value) =>
_requiredLength(value, min: 3, label: 'Title'),
),
const SizedBox(height: 16),
AppTextField(
controller: _subtitle,
label: 'Summary',
hint: 'One sentence overview',
textInputAction: TextInputAction.next,
validator: (value) =>
_requiredLength(value, min: 8, label: 'Summary'),
),
const SizedBox(height: 16),
DropdownButtonFormField<ItemCategory>(
initialValue: _category,
decoration: const InputDecoration(labelText: 'Category'),
items: [
for (final category in ItemCategory.values)
DropdownMenuItem(
value: category,
child: Text(category.label),
),
],
onChanged: (value) => setState(() => _category = value),
validator: (value) =>
value == null ? 'Choose a category.' : null,
),
const SizedBox(height: 16),
AppTextField(
controller: _description,
label: 'Description',
hint: 'A longer note the detail screen will show',
maxLines: 5,
keyboardType: TextInputType.multiline,
validator: (value) =>
_requiredLength(value, min: 16, label: 'Description'),
),
const SizedBox(height: 24),
FilledButton(
onPressed: _submit,
child: const Text('Save item'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
const SizedBox(height: 24),
],
),
),
),
);
}
}