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
+95 -66
View File
@@ -10,8 +10,10 @@ import 'services/app_settings.dart';
import 'services/product_repository.dart';
import 'utils/fuzzy.dart';
import 'utils/money.dart';
import 'utils/product_names.dart';
import 'widgets/confirm_dialog.dart';
import 'widgets/product_image.dart';
import 'widgets/product_name_conflicts.dart';
import 'widgets/profile_button.dart';
import 'widgets/settings_button.dart';
import 'widgets/store_tag.dart';
@@ -27,11 +29,14 @@ class ProductsScreen extends StatefulWidget {
class _ProductsScreenState extends State<ProductsScreen> {
ProductSort _sort = ProductSort.preferred;
bool _favouritesOnly = false;
bool _searching = false;
bool _editing = false;
final TextEditingController _search = TextEditingController();
List<Product>? _products;
ProductNameIssues _nameIssues = const ProductNameIssues(
mergeGroups: [],
spellingVariants: [],
);
Object? _error;
int _loadToken = 0;
@@ -55,13 +60,12 @@ class _ProductsScreenState extends State<ProductsScreen> {
Future<void> _load() async {
final token = ++_loadToken;
try {
final products = await widget.repository.getAll(
sort: _sort,
favouritesOnly: _favouritesOnly,
);
final products = await widget.repository.getAll(sort: _sort);
final nameIssues = await widget.repository.getProductNameIssues();
if (!mounted || token != _loadToken) return;
setState(() {
_products = products;
_nameIssues = nameIssues;
_error = null;
if (products.isEmpty) {
_editing = false;
@@ -139,8 +143,7 @@ class _ProductsScreenState extends State<ProductsScreen> {
final confirmed = await confirmAction(
context,
title: 'Delete all products?',
message:
'This removes every product from your catalog. Shopping trips stay in the shop log.',
message: 'This removes every product from your catalog. Shopping trips stay in the shop log.',
);
if (!confirmed || !mounted) return;
setState(() {
@@ -156,28 +159,31 @@ class _ProductsScreenState extends State<ProductsScreen> {
}
}
Set<int> get _issueProductIds => _nameIssues.productIds;
Future<void> _resolveNameIssues({int? focusProductId}) async {
final issues = await widget.repository.getProductNameIssues();
if (!mounted || issues.isEmpty) return;
final resolved = await showProductNameConflictSheet(
context,
repository: widget.repository,
issues: issues,
focusProductId: focusProductId,
);
if (resolved && mounted) _load();
}
Future<void> _toggleFavourite(Product product) async {
final next = !product.favourite;
setState(() {
_products = [
for (final item in _products ?? const <Product>[])
if (item.id == product.id)
item.copyWith(favourite: next)
else
item,
if (item.id == product.id) item.copyWith(favourite: next) else item,
];
if (_favouritesOnly && !next) {
_products = [
for (final item in _products!)
if (item.id != product.id) item,
];
if (_products!.isEmpty) {
_editing = false;
}
}
});
try {
await widget.repository.setFavourite(product.id, next);
if (_sort == ProductSort.start && mounted) _load();
} catch (_) {
if (mounted) _load();
}
@@ -187,7 +193,9 @@ class _ProductsScreenState extends State<ProductsScreen> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final stores = SettingsScope.maybeOf(context)?.supermarkets ?? const [];
final products = _products == null ? const <Product>[] : _filter(_products!);
final products = _products == null
? const <Product>[]
: _filter(_products!);
return Scaffold(
appBar: AppBar(
@@ -218,6 +226,12 @@ class _ProductsScreenState extends State<ProductsScreen> {
: null,
icon: Icon(_searching ? Icons.close : Icons.search),
),
if (_hasProducts && _nameIssues.isNotEmpty && !_editing)
IconButton(
tooltip: 'Resolve name issues',
onPressed: _resolveNameIssues,
icon: const Icon(kProductNameConflictIcon),
),
if (_hasProducts)
IconButton(
tooltip: _editing ? 'Done' : 'Edit',
@@ -243,46 +257,35 @@ class _ProductsScreenState extends State<ProductsScreen> {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 4),
child: Row(
children: [
Expanded(
child: SegmentedButton<ProductSort>(
segments: const [
ButtonSegment(
value: ProductSort.preferred,
label: Text('Preferred'),
),
ButtonSegment(
value: ProductSort.name,
label: Text('A–Z'),
),
],
selected: {_sort},
onSelectionChanged: (selected) {
setState(() {
_sort = selected.first;
});
_load();
},
),
child: SegmentedButton<ProductSort>(
segments: const [
ButtonSegment(
value: ProductSort.name,
label: Text('A–Z'),
),
const SizedBox(width: 8),
IconButton.filledTonal(
tooltip: 'Favourites',
isSelected: _favouritesOnly,
onPressed: () {
setState(() {
_favouritesOnly = !_favouritesOnly;
});
_load();
},
icon: Icon(
_favouritesOnly ? Icons.star : Icons.star_outline,
),
ButtonSegment(
value: ProductSort.preferred,
label: Text('Preferred'),
),
ButtonSegment(
value: ProductSort.start,
label: Text('Start'),
),
],
selected: {_sort},
onSelectionChanged: (selected) {
setState(() {
_sort = selected.first;
});
_load();
},
),
),
if (_nameIssues.isNotEmpty && !_editing)
ProductNameConflictBanner(
issues: _nameIssues,
onTap: _resolveNameIssues,
),
Expanded(child: _buildList(theme, stores, products)),
],
),
@@ -308,9 +311,7 @@ class _ProductsScreenState extends State<ProductsScreen> {
if (_products!.isEmpty) {
return Center(
child: Text(
_favouritesOnly
? 'Star products to keep them as favourites.'
: 'Confirmed receipt items will show up here.',
'Confirmed receipt items will show up here.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),
@@ -332,6 +333,7 @@ class _ProductsScreenState extends State<ProductsScreen> {
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final product = products[index];
final hasIssue = _issueProductIds.contains(product.id);
return ListTile(
dense: true,
visualDensity: VisualDensity.compact,
@@ -372,10 +374,25 @@ class _ProductsScreenState extends State<ProductsScreen> {
),
],
)
: ProductImage(
name: product.name,
path: product.imagePath,
size: 36,
: Stack(
clipBehavior: Clip.none,
children: [
ProductImage(
name: product.name,
path: product.imagePath,
size: 36,
),
if (hasIssue && !_editing)
Positioned(
right: -2,
top: -2,
child: ProductNameConflictMarker(
compact: true,
onTap: () =>
_resolveNameIssues(focusProductId: product.id),
),
),
],
),
title: Text(
product.name,
@@ -395,9 +412,21 @@ class _ProductsScreenState extends State<ProductsScreen> {
),
),
),
trailing: Text(
formatEuro(product.lastPrice),
style: theme.textTheme.titleMedium,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (hasIssue && !_editing)
Padding(
padding: const EdgeInsets.only(right: 4),
child: ProductNameConflictMarker(
onTap: () => _resolveNameIssues(focusProductId: product.id),
),
),
Text(
formatEuro(product.lastPrice),
style: theme.textTheme.titleMedium,
),
],
),
onTap: _editing ? null : () => _openProduct(product),
);