/// Items on one shopping list, with check-off and edit/remove. library; import 'package:flutter/material.dart'; import 'models/shopping_list.dart'; import 'product_detail_screen.dart'; import 'services/product_repository.dart'; import 'utils/dates.dart'; import 'widgets/add_list_items_sheet.dart'; import 'widgets/confirm_dialog.dart'; import 'widgets/name_dialog.dart'; class ShoppingListDetailScreen extends StatefulWidget { const ShoppingListDetailScreen({ super.key, required this.repository, required this.listId, }); final ProductRepository repository; final int listId; @override State createState() => _ShoppingListDetailScreenState(); } class _ShoppingListDetailScreenState extends State { ShoppingList? _list; List? _items; Object? _error; bool _editing = false; int _loadToken = 0; @override void initState() { super.initState(); widget.repository.addListener(_load); _load(); } @override void dispose() { widget.repository.removeListener(_load); super.dispose(); } Future _load() async { final token = ++_loadToken; try { final list = await widget.repository.getShoppingList(widget.listId); if (list == null) throw StateError('List not found'); final items = await widget.repository.getShoppingListItems(widget.listId); if (!mounted || token != _loadToken) return; setState(() { _list = list; _items = items; _error = null; }); } catch (error) { if (!mounted || token != _loadToken) return; setState(() => _error = error); } } Future _rename(ShoppingList list) async { final name = await showNameDialog( context, title: 'Rename list', initial: list.name, label: 'List name', ); if (name == null || !mounted) return; await widget.repository.renameShoppingList(widget.listId, name); if (mounted) { setState( () => _list = ShoppingList( id: list.id, name: name, itemCount: list.itemCount, checkedCount: list.checkedCount, createdAt: list.createdAt, updatedAt: DateTime.now(), ), ); _load(); } } Future _openAddSheet() async { final products = await widget.repository.getAll(); if (!mounted) return; final selections = await showAddListItemsSheet(context, products: products); if (selections == null || selections.isEmpty || !mounted) return; final pending = [ for (var i = 0; i < selections.length; i++) ShoppingListItem( id: -(i + 1), listId: widget.listId, name: selections[i].name, checked: false, sortOrder: (_items?.length ?? 0) + i, productId: selections[i].productId, ), ]; setState(() { _items = [...?_items, ...pending]; if (_list != null) { _list = ShoppingList( id: _list!.id, name: _list!.name, itemCount: _list!.itemCount + pending.length, checkedCount: _list!.checkedCount, createdAt: _list!.createdAt, updatedAt: DateTime.now(), ); } }); try { final added = await widget.repository.addShoppingListItems( listId: widget.listId, items: [ for (final selection in selections) (name: selection.name, productId: selection.productId), ], ); if (!mounted) return; setState(() { final withoutPending = _items ?.where((item) => item.id >= 0) .toList(growable: false); _items = [...?withoutPending, ...added]; }); } catch (_) { if (mounted) _load(); } } Future _editItem(ShoppingListItem item) async { final name = await showNameDialog( context, title: 'Edit item', initial: item.name, label: 'Item', ); if (name == null || !mounted) return; await widget.repository.updateShoppingListItem(id: item.id, name: name); if (mounted) _load(); } Future _removeItem(ShoppingListItem item) async { final confirmed = await confirmAction( context, title: 'Remove item?', message: '"${item.name}" will be removed from this list.', confirmLabel: 'Remove', ); if (!confirmed || !mounted) return; setState(() { _items = [ for (final entry in _items ?? const []) if (entry.id != item.id) entry, ]; if (_list != null) { _list = ShoppingList( id: _list!.id, name: _list!.name, itemCount: _list!.itemCount - 1, checkedCount: item.checked ? _list!.checkedCount - 1 : _list!.checkedCount, createdAt: _list!.createdAt, updatedAt: DateTime.now(), ); } }); try { await widget.repository.deleteShoppingListItem(item.id); } catch (_) { if (mounted) _load(); } } Future _toggle(ShoppingListItem item) async { final next = !item.checked; setState(() { _items = [ for (final entry in _items ?? const []) if (entry.id == item.id) ShoppingListItem( id: entry.id, listId: entry.listId, name: entry.name, checked: next, sortOrder: entry.sortOrder, productId: entry.productId, ) else entry, ]; if (_list != null) { _list = ShoppingList( id: _list!.id, name: _list!.name, itemCount: _list!.itemCount, checkedCount: _list!.checkedCount + (next ? 1 : -1), createdAt: _list!.createdAt, updatedAt: _list!.updatedAt, ); } }); try { await widget.repository.updateShoppingListItem( id: item.id, checked: next, ); } catch (_) { if (mounted) _load(); } } Future _shopAgain() async { final list = _list; if (list == null || list.checkedCount == 0) return; setState(() { _items = [ for (final item in _items ?? const []) ShoppingListItem( id: item.id, listId: item.listId, name: item.name, checked: false, sortOrder: item.sortOrder, productId: item.productId, ), ]; _list = ShoppingList( id: list.id, name: list.name, itemCount: list.itemCount, checkedCount: 0, createdAt: list.createdAt, updatedAt: DateTime.now(), ); }); try { await widget.repository.resetShoppingListChecks(widget.listId); } catch (_) { if (mounted) _load(); } } Future _openProduct(int productId) async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => ProductDetailScreen( repository: widget.repository, productId: productId, ), ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); if (_error != null && _list == null) { return Scaffold( appBar: AppBar(), body: Center( child: Text( 'Could not load this list.', style: theme.textTheme.bodyLarge, ), ), ); } if (_list == null || _items == null) { return Scaffold( appBar: AppBar(), body: const Center(child: CircularProgressIndicator()), ); } final list = _list!; final items = _items!; return Scaffold( appBar: AppBar( title: Text('${list.name} (${formatDate(list.createdAt)})'), actions: [ if (list.checkedCount > 0 && !_editing) TextButton( onPressed: _shopAgain, child: const Text('Shop again'), ), IconButton( tooltip: 'Rename', onPressed: () => _rename(list), icon: const Icon(Icons.drive_file_rename_outline), ), IconButton( tooltip: _editing ? 'Done' : 'Edit', onPressed: () => setState(() => _editing = !_editing), icon: Icon(_editing ? Icons.check : Icons.edit_outlined), ), ], ), floatingActionButton: FloatingActionButton( onPressed: _openAddSheet, tooltip: 'Add item', child: const Icon(Icons.add), ), body: items.isEmpty ? Center( child: Text( 'Add things to buy.', style: theme.textTheme.bodyLarge, ), ) : ListView.builder( padding: const EdgeInsets.fromLTRB(8, 8, 8, 88), itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ListTile( leading: Checkbox( value: item.checked, onChanged: _editing || item.id < 0 ? null : (_) => _toggle(item), ), title: Text( item.name, style: item.checked ? theme.textTheme.bodyLarge?.copyWith( decoration: TextDecoration.lineThrough, color: theme.colorScheme.onSurfaceVariant, ) : null, ), trailing: _editing ? Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'Edit', onPressed: item.id < 0 ? null : () => _editItem(item), icon: const Icon(Icons.edit_outlined), ), IconButton( tooltip: 'Remove', onPressed: item.id < 0 ? null : () => _removeItem(item), icon: const Icon(Icons.delete_outline), ), ], ) : item.productId == null ? null : IconButton( tooltip: 'Product', onPressed: () => _openProduct(item.productId!), icon: const Icon(Icons.chevron_right), ), onTap: _editing || item.id < 0 ? null : () => _toggle(item), ); }, ), ); } }