fixes before build

This commit is contained in:
gitea
2026-08-31 16:51:31 +02:00
parent 7692809219
commit 756f9db356
34 changed files with 3542 additions and 837 deletions
+170 -32
View File
@@ -31,15 +31,18 @@ class ShopDetailScreen extends StatefulWidget {
}
class _ShopDetailScreenState extends State<ShopDetailScreen> {
late Future<({Shop shop, List<ShopItem> items})> _future;
Shop? _shop;
List<ShopItem>? _items;
Object? _error;
bool _editing = false;
List<String> _tagSuggestions = [];
int _loadToken = 0;
@override
void initState() {
super.initState();
_future = _load();
widget.repository.addListener(_reload);
widget.repository.addListener(_load);
_load();
widget.repository
.getUsedShopTags()
.then((tags) {
@@ -50,22 +53,26 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
@override
void dispose() {
widget.repository.removeListener(_reload);
widget.repository.removeListener(_load);
super.dispose();
}
Future<({Shop shop, List<ShopItem> items})> _load() async {
final shop = await widget.repository.getShop(widget.shopId);
if (shop == null) {
throw StateError('Shop not found');
Future<void> _load() async {
final token = ++_loadToken;
try {
final shop = await widget.repository.getShop(widget.shopId);
if (shop == null) throw StateError('Shop not found');
final items = await widget.repository.getShopItems(widget.shopId);
if (!mounted || token != _loadToken) return;
setState(() {
_shop = shop;
_items = items;
_error = null;
});
} catch (error) {
if (!mounted || token != _loadToken) return;
setState(() => _error = error);
}
final items = await widget.repository.getShopItems(widget.shopId);
return (shop: shop, items: items);
}
void _reload() {
if (!mounted) return;
setState(() => _future = _load());
}
Future<void> _openReceipt(File file) async {
@@ -93,6 +100,56 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
);
}
Future<void> _addItem() async {
final shop = _shop;
if (shop == null) return;
final result = await showItemEditSheet(context);
if (result == null || !mounted) return;
final pendingId = -DateTime.now().microsecondsSinceEpoch;
final pending = ShopItem(
id: pendingId,
shopId: shop.id,
name: result.name,
price: result.price,
quantity: result.quantity,
unitPrice: result.unitPrice,
);
setState(() {
_items = [...?_items, pending];
_shop = Shop(
id: shop.id,
supermarket: shop.supermarket,
shoppedAt: shop.shoppedAt,
total: roundMoney(shop.total + result.price),
itemCount: shop.itemCount + 1,
name: shop.name,
receiptImagePath: shop.receiptImagePath,
tags: shop.tags,
);
});
try {
final saved = await widget.repository.addShopItem(
shopId: shop.id,
item: result,
);
if (!mounted) return;
setState(() {
_items = [
for (final entry in _items ?? const <ShopItem>[])
if (entry.id == pendingId) saved else entry,
];
});
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not add this product.')),
);
_load();
}
}
Future<void> _editItem(ShopItem item) async {
final result = await showItemEditSheet(
context,
@@ -112,7 +169,7 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
quantity: result.quantity,
unitPrice: result.unitPrice,
);
if (mounted) _reload();
if (mounted) _load();
}
Future<void> _removeItem(ShopItem item) async {
@@ -124,8 +181,59 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
confirmLabel: 'Remove',
);
if (!confirmed || !mounted) return;
await widget.repository.deleteShopItem(item.id);
if (mounted) _reload();
final isLastItem = (_items?.length ?? 0) == 1;
final shop = _shop;
setState(() {
_items = [
for (final entry in _items ?? const <ShopItem>[])
if (entry.id != item.id) entry,
];
if (shop != null) {
_shop = Shop(
id: shop.id,
supermarket: shop.supermarket,
shoppedAt: shop.shoppedAt,
total: roundMoney(shop.total - item.price),
itemCount: shop.itemCount - 1,
name: shop.name,
receiptImagePath: shop.receiptImagePath,
tags: shop.tags,
);
}
});
try {
await widget.repository.deleteShopItem(item.id);
if (!mounted || !isLastItem || shop == null) return;
await _confirmRemoveEmptyTrip(shop);
} catch (_) {
if (mounted) _load();
}
}
Future<void> _confirmRemoveEmptyTrip(Shop shop) async {
final removeTrip = await confirmAction(
context,
title: 'Remove trip?',
message:
'This trip has no products left. Remove it from your shop log? '
'Products from this trip stay in your catalog.',
confirmLabel: 'Remove trip',
);
if (!mounted) return;
if (!removeTrip) return;
try {
await widget.repository.deleteShop(shop.id);
if (mounted) Navigator.of(context).pop();
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not remove this trip.')),
);
_load();
}
}
Future<void> _editName(Shop shop) async {
@@ -142,12 +250,27 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
name: name,
clearName: name.isEmpty,
);
if (mounted) _reload();
if (mounted) _load();
}
Future<void> _editDate(Shop shop) async {
final picked = await showDatePicker(
context: context,
initialDate: shop.shoppedAt,
firstDate: DateTime(2018),
lastDate: DateTime.now().add(const Duration(days: 1)),
);
if (picked == null || !mounted) return;
await widget.repository.updateShop(
id: shop.id,
shoppedAt: DateTime(picked.year, picked.month, picked.day),
);
if (mounted) _load();
}
Future<void> _setTags(Shop shop, List<String> tags) async {
await widget.repository.updateShop(id: shop.id, tags: tags);
if (mounted) _reload();
if (mounted) _load();
}
Widget? _itemSubtitle(ShopItem item) {
@@ -175,10 +298,16 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
),
],
),
body: FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (snapshot.hasError) {
floatingActionButton: _editing
? FloatingActionButton.extended(
onPressed: _addItem,
icon: const Icon(Icons.add),
label: const Text('Add product'),
)
: null,
body: Builder(
builder: (context) {
if (_error != null) {
return Center(
child: Text(
'Could not load this trip.',
@@ -186,18 +315,18 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
),
);
}
if (!snapshot.hasData) {
final shop = _shop;
final items = _items;
if (shop == null || items == null) {
return const Center(child: CircularProgressIndicator());
}
final shop = snapshot.data!.shop;
final items = snapshot.data!.items;
final receipt = shop.receiptImagePath;
final receiptFile = receipt == null ? null : File(receipt);
final hasReceipt = receiptFile != null && receiptFile.existsSync();
return ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 96),
children: [
if (_editing)
ListTile(
@@ -218,10 +347,19 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
Text(shop.supermarket, style: theme.textTheme.titleMedium),
],
const SizedBox(height: 4),
Text(
formatDate(shop.shoppedAt),
style: theme.textTheme.bodyLarge,
),
if (_editing)
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(formatDate(shop.shoppedAt)),
subtitle: const Text('Trip date'),
trailing: const Icon(Icons.calendar_today_outlined),
onTap: () => _editDate(shop),
)
else
Text(
formatDate(shop.shoppedAt),
style: theme.textTheme.bodyLarge,
),
if (_editing) ...[
const SizedBox(height: 16),
TagEditor(