/// Manage supermarket names, colors, and bonus card photos. library; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'models/supermarket.dart'; import 'services/app_settings.dart'; import 'services/product_repository.dart'; import 'widgets/store_edit_dialog.dart'; import 'widgets/store_tag.dart'; class SupermarketsScreen extends StatefulWidget { const SupermarketsScreen({super.key, required this.repository}); final ProductRepository repository; @override State createState() => _SupermarketsScreenState(); } class _SupermarketsScreenState extends State { List? _stores; bool _busy = false; final ImagePicker _picker = ImagePicker(); @override void initState() { super.initState(); widget.repository.addListener(_load); _load(); } @override void dispose() { widget.repository.removeListener(_load); super.dispose(); } Future _load() async { try { final stores = await widget.repository.getSupermarkets(); if (!mounted) return; setState(() => _stores = stores); } catch (_) { if (!mounted) return; setState(() => _stores ??= const []); } } Future _syncSettings() async { await _settings?.reload(); } AppSettings? get _settings => SettingsScope.maybeOf(context); Future _editStore(Supermarket? existing) async { final result = await showStoreEditDialog(context, store: existing); if (result == null) return; setState(() => _busy = true); try { final settings = _settings; if (settings == null) return; if (existing == null) { await settings.addSupermarket(result.name, result.colorValue); } else { await settings.updateSupermarket(result); } await _load(); await _syncSettings(); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('That supermarket already exists.')), ); } finally { if (mounted) setState(() => _busy = false); } } Future _deleteStore(Supermarket store) async { if (store.id == null) return; setState(() => _busy = true); try { await _settings?.deleteSupermarket(store.id!); await _load(); await _syncSettings(); } finally { if (mounted) setState(() => _busy = false); } } Future _pickBonusCard(Supermarket store) async { if (store.id == null) return; final source = await showModalBottomSheet( context: context, showDragHandle: true, builder: (context) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.photo_camera_outlined), title: const Text('Take photo'), onTap: () => Navigator.pop(context, ImageSource.camera), ), ListTile( leading: const Icon(Icons.photo_library_outlined), title: const Text('Pick from gallery'), onTap: () => Navigator.pop(context, ImageSource.gallery), ), ], ), ); }, ); if (source == null) return; final file = await _picker.pickImage(source: source, imageQuality: 85); if (file == null) return; setState(() => _busy = true); try { await widget.repository.setSupermarketBonusCard(store.id!, file.path); await _load(); await _syncSettings(); } finally { if (mounted) setState(() => _busy = false); } } Future _viewBonusCard(String path) async { await showDialog( context: context, builder: (context) { return Dialog( insetPadding: const EdgeInsets.all(16), child: InteractiveViewer( child: Image.file(File(path), fit: BoxFit.contain), ), ); }, ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final stores = _stores; return Scaffold( appBar: AppBar(title: const Text('Supermarkets')), floatingActionButton: FloatingActionButton( onPressed: _busy ? null : () => _editStore(null), tooltip: 'Add supermarket', child: const Icon(Icons.add), ), body: stores == null ? const Center(child: CircularProgressIndicator()) : ListView.separated( padding: const EdgeInsets.fromLTRB(8, 8, 8, 88), itemCount: stores.length + 1, separatorBuilder: (_, index) => index == 0 ? const SizedBox(height: 4) : const Divider(height: 1), itemBuilder: (context, index) { if (index == 0) { return Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Store names and colors appear on products. ' 'Add a bonus card photo when you have one.', style: theme.textTheme.bodyMedium, ), if (_busy) ...[ const SizedBox(height: 8), const LinearProgressIndicator(), ], ], ), ); } final store = stores[index - 1]; return _SupermarketRow( store: store, stores: stores, busy: _busy, onEdit: () => _editStore(store), onDelete: store.id == null ? null : () => _deleteStore(store), onBonusCard: () => _pickBonusCard(store), onViewBonusCard: store.bonusImagePath == null ? null : () => _viewBonusCard(store.bonusImagePath!), ); }, ), ); } } class _SupermarketRow extends StatelessWidget { const _SupermarketRow({ required this.store, required this.stores, required this.busy, required this.onEdit, required this.onBonusCard, this.onDelete, this.onViewBonusCard, }); final Supermarket store; final List stores; final bool busy; final VoidCallback onEdit; final VoidCallback? onDelete; final VoidCallback onBonusCard; final VoidCallback? onViewBonusCard; @override Widget build(BuildContext context) { final path = store.bonusImagePath; final hasCard = path != null && path.isNotEmpty && File(path).existsSync(); return ListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8), leading: hasCard ? InkWell( onTap: busy ? null : onViewBonusCard, borderRadius: BorderRadius.circular(8), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file( File(path), width: 44, height: 28, fit: BoxFit.cover, ), ), ) : CircleAvatar( radius: 16, backgroundColor: store.color, child: Text( store.name.isEmpty ? '?' : store.name.substring(0, 1), style: TextStyle( color: store.color.computeLuminance() > 0.55 ? Colors.black87 : Colors.white, fontSize: 13, fontWeight: FontWeight.w700, ), ), ), title: StoreTag(name: store.name, stores: stores, compact: true), subtitle: hasCard ? null : const Text('No bonus card'), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: hasCard ? 'Replace bonus card' : 'Add bonus card', onPressed: busy ? null : onBonusCard, icon: Icon(hasCard ? Icons.photo_camera : Icons.card_membership_outlined), ), IconButton( tooltip: 'Edit', onPressed: busy ? null : onEdit, icon: const Icon(Icons.edit_outlined), ), IconButton( tooltip: 'Delete', onPressed: busy ? null : onDelete, icon: const Icon(Icons.delete_outline), ), ], ), ); } }