176 lines
5.6 KiB
Dart
176 lines
5.6 KiB
Dart
/// Multi-select sheet for adding catalog products to a shopping list.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../models/product.dart';
|
|
import 'product_image.dart';
|
|
|
|
class AddListItemSelection {
|
|
const AddListItemSelection({required this.name, this.productId});
|
|
|
|
final String name;
|
|
final int? productId;
|
|
}
|
|
|
|
Future<List<AddListItemSelection>?> showAddListItemsSheet(
|
|
BuildContext context, {
|
|
required List<Product> products,
|
|
}) {
|
|
return showModalBottomSheet<List<AddListItemSelection>>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
showDragHandle: true,
|
|
builder: (context) => _AddListItemsSheet(products: products),
|
|
);
|
|
}
|
|
|
|
class _AddListItemsSheet extends StatefulWidget {
|
|
const _AddListItemsSheet({required this.products});
|
|
|
|
final List<Product> products;
|
|
|
|
@override
|
|
State<_AddListItemsSheet> createState() => _AddListItemsSheetState();
|
|
}
|
|
|
|
class _AddListItemsSheetState extends State<_AddListItemsSheet> {
|
|
final TextEditingController _controller = TextEditingController();
|
|
final Set<int> _selectedProductIds = {};
|
|
String? _error;
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
List<Product> get _matches {
|
|
final query = _controller.text.trim().toLowerCase();
|
|
if (query.isEmpty) return widget.products;
|
|
return widget.products
|
|
.where((product) => product.name.toLowerCase().contains(query))
|
|
.toList();
|
|
}
|
|
|
|
int get _addCount {
|
|
var count = _selectedProductIds.length;
|
|
final typed = _controller.text.trim();
|
|
if (typed.isEmpty) return count;
|
|
final selectedNames = {
|
|
for (final product in widget.products)
|
|
if (_selectedProductIds.contains(product.id))
|
|
product.name.toLowerCase(),
|
|
};
|
|
if (!selectedNames.contains(typed.toLowerCase())) count++;
|
|
return count;
|
|
}
|
|
|
|
void _toggleProduct(Product product) {
|
|
setState(() {
|
|
if (_selectedProductIds.contains(product.id)) {
|
|
_selectedProductIds.remove(product.id);
|
|
} else {
|
|
_selectedProductIds.add(product.id);
|
|
}
|
|
_error = null;
|
|
});
|
|
}
|
|
|
|
void _add() {
|
|
final selections = <AddListItemSelection>[
|
|
for (final product in widget.products)
|
|
if (_selectedProductIds.contains(product.id))
|
|
AddListItemSelection(name: product.name, productId: product.id),
|
|
];
|
|
final typed = _controller.text.trim();
|
|
if (typed.isNotEmpty &&
|
|
!selections.any(
|
|
(item) => item.name.toLowerCase() == typed.toLowerCase(),
|
|
)) {
|
|
selections.add(AddListItemSelection(name: typed));
|
|
}
|
|
if (selections.isEmpty) {
|
|
setState(() => _error = 'Select products or enter an item name.');
|
|
return;
|
|
}
|
|
Navigator.of(context).pop(selections);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
|
final matches = _matches;
|
|
final addCount = _addCount;
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
|
child: SizedBox(
|
|
height: MediaQuery.sizeOf(context).height * 0.72,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text('Add items', style: theme.textTheme.titleLarge),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _controller,
|
|
autofocus: true,
|
|
textCapitalization: TextCapitalization.sentences,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Search or type a new item',
|
|
),
|
|
onChanged: (_) => setState(() => _error = null),
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
|
],
|
|
const SizedBox(height: 8),
|
|
Text('From products', style: theme.textTheme.labelMedium),
|
|
const SizedBox(height: 4),
|
|
Expanded(
|
|
child: matches.isEmpty
|
|
? Center(
|
|
child: Text(
|
|
widget.products.isEmpty
|
|
? 'No products in your catalog yet.'
|
|
: 'No products match this search.',
|
|
style: theme.textTheme.bodyMedium,
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: matches.length,
|
|
itemBuilder: (context, index) {
|
|
final product = matches[index];
|
|
final selected = _selectedProductIds.contains(
|
|
product.id,
|
|
);
|
|
return ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: Checkbox(
|
|
value: selected,
|
|
onChanged: (_) => _toggleProduct(product),
|
|
),
|
|
title: Text(product.name),
|
|
trailing: ProductImage(
|
|
name: product.name,
|
|
path: product.imagePath,
|
|
size: 32,
|
|
),
|
|
onTap: () => _toggleProduct(product),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
FilledButton(
|
|
onPressed: addCount == 0 ? null : _add,
|
|
child: Text(addCount == 1 ? 'Add 1 item' : 'Add $addCount items'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|