Files
receipity/lib/supermarkets_screen.dart
T
2026-08-31 16:51:31 +02:00

285 lines
8.6 KiB
Dart

/// 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<SupermarketsScreen> createState() => _SupermarketsScreenState();
}
class _SupermarketsScreenState extends State<SupermarketsScreen> {
List<Supermarket>? _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<void> _load() async {
try {
final stores = await widget.repository.getSupermarkets();
if (!mounted) return;
setState(() => _stores = stores);
} catch (_) {
if (!mounted) return;
setState(() => _stores ??= const []);
}
}
Future<void> _syncSettings() async {
await _settings?.reload();
}
AppSettings? get _settings => SettingsScope.maybeOf(context);
Future<void> _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<void> _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<void> _pickBonusCard(Supermarket store) async {
if (store.id == null) return;
final source = await showModalBottomSheet<ImageSource>(
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<void> _viewBonusCard(String path) async {
await showDialog<void>(
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<Supermarket> 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),
),
],
),
);
}
}