mirror of
https://github.com/chenasraf/pantry-flutter.git
synced 2026-05-17 17:28:03 +00:00
380 lines
11 KiB
Dart
380 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:pantry/i18n.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:pantry/models/checklist.dart';
|
|
import 'package:pantry/utils/checklist_icons.dart';
|
|
import 'package:pantry/widgets/checklist_selector.dart';
|
|
import 'package:pantry/widgets/checklist_sort_button.dart';
|
|
import 'package:pantry/widgets/create_list_dialog.dart';
|
|
import 'checklist_item_tile.dart';
|
|
import 'checklists_controller.dart';
|
|
import 'item_detail_view.dart';
|
|
import 'item_form_view.dart';
|
|
|
|
class ChecklistsView extends StatefulWidget {
|
|
final int houseId;
|
|
|
|
const ChecklistsView({super.key, required this.houseId});
|
|
|
|
@override
|
|
State<ChecklistsView> createState() => _ChecklistsViewState();
|
|
}
|
|
|
|
class _ChecklistsViewState extends State<ChecklistsView> {
|
|
late final _controller = ChecklistsController(houseId: widget.houseId);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller.load();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ChangeNotifierProvider.value(
|
|
value: _controller,
|
|
child: const _ChecklistsBody(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ChecklistsBody extends StatelessWidget {
|
|
const _ChecklistsBody();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final controller = context.watch<ChecklistsController>();
|
|
|
|
if (controller.isLoading && controller.lists.isEmpty) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (controller.error != null && controller.lists.isEmpty) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(controller.error!, textAlign: TextAlign.center),
|
|
const SizedBox(height: 16),
|
|
FilledButton(
|
|
onPressed: controller.load,
|
|
child: Text(m.common.retry),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (controller.lists.isEmpty) {
|
|
return Center(child: Text(m.checklists.noChecklists));
|
|
}
|
|
|
|
Widget itemsArea;
|
|
if (controller.isLoading) {
|
|
itemsArea = const Center(child: CircularProgressIndicator());
|
|
} else if (controller.error != null) {
|
|
itemsArea = Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(controller.error!, textAlign: TextAlign.center),
|
|
const SizedBox(height: 16),
|
|
FilledButton(
|
|
onPressed: controller.load,
|
|
child: Text(m.common.retry),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
itemsArea = RefreshIndicator(
|
|
onRefresh: controller.refresh,
|
|
child: _ItemList(controller: controller),
|
|
);
|
|
}
|
|
|
|
return Stack(
|
|
children: [
|
|
Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ChecklistSelector(
|
|
lists: controller.lists,
|
|
currentList: controller.currentList,
|
|
onSelected: controller.selectList,
|
|
),
|
|
),
|
|
ChecklistSortButton(
|
|
currentSort: controller.sortBy,
|
|
onSelected: controller.setSortBy,
|
|
),
|
|
],
|
|
),
|
|
Expanded(child: itemsArea),
|
|
],
|
|
),
|
|
if (controller.currentList != null)
|
|
Positioned(
|
|
right: 16,
|
|
bottom: 16,
|
|
child: FloatingActionButton(
|
|
onPressed: () async {
|
|
final added = await Navigator.of(context).push<bool>(
|
|
MaterialPageRoute(
|
|
builder: (_) => ItemFormView(controller: controller),
|
|
),
|
|
);
|
|
if (added == true) {
|
|
controller.refresh();
|
|
}
|
|
},
|
|
child: const Icon(Icons.add),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ItemList extends StatelessWidget {
|
|
final ChecklistsController controller;
|
|
|
|
const _ItemList({required this.controller});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final unchecked = controller.items.where((i) => !i.done).toList();
|
|
final checked = controller.items.where((i) => i.done).toList();
|
|
|
|
if (controller.items.isEmpty) {
|
|
return ListView(
|
|
children: [
|
|
const SizedBox(height: 100),
|
|
Center(child: Text(m.checklists.noItems)),
|
|
],
|
|
);
|
|
}
|
|
|
|
return CustomScrollView(
|
|
slivers: [
|
|
_ReorderablePartition(items: unchecked, controller: controller),
|
|
if (checked.isNotEmpty) ...[
|
|
SliverToBoxAdapter(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Divider(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 4,
|
|
),
|
|
child: Text(
|
|
m.checklists.completedCount(checked.length),
|
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_ReorderablePartition(items: checked, controller: controller),
|
|
],
|
|
const SliverToBoxAdapter(child: SizedBox(height: 88)),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ReorderablePartition extends StatelessWidget {
|
|
final List<ListItem> items;
|
|
final ChecklistsController controller;
|
|
|
|
const _ReorderablePartition({required this.items, required this.controller});
|
|
|
|
void _viewItem(
|
|
BuildContext context,
|
|
ChecklistsController controller,
|
|
ListItem item,
|
|
) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (_) => ItemDetailView(
|
|
item: item,
|
|
category: item.categoryId != null
|
|
? controller.categories[item.categoryId]
|
|
: null,
|
|
houseId: controller.houseId,
|
|
controller: controller,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _moveItem(
|
|
BuildContext context,
|
|
ChecklistsController controller,
|
|
ListItem item,
|
|
) {
|
|
final otherLists = controller.lists
|
|
.where((l) => l.id != controller.currentList?.id)
|
|
.toList();
|
|
|
|
showDialog<int>(
|
|
context: context,
|
|
builder: (ctx) => SimpleDialog(
|
|
title: Text(m.checklists.moveItem),
|
|
children: [
|
|
...otherLists.map(
|
|
(list) => SimpleDialogOption(
|
|
onPressed: () => Navigator.pop(ctx, list.id),
|
|
child: Row(
|
|
children: [
|
|
Icon(checklistIcon(list.icon), size: 20),
|
|
const SizedBox(width: 12),
|
|
Text(list.name),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const Divider(),
|
|
SimpleDialogOption(
|
|
onPressed: () async {
|
|
Navigator.pop(ctx);
|
|
final created = await showCreateListDialog(context, controller);
|
|
if (created != null && context.mounted) {
|
|
try {
|
|
await controller.moveItem(item, created.id);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(m.checklists.moveFailed)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.add,
|
|
size: 20,
|
|
color: Theme.of(ctx).colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
m.checklists.createList,
|
|
style: TextStyle(color: Theme.of(ctx).colorScheme.primary),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
).then((targetListId) async {
|
|
if (targetListId == null) return;
|
|
try {
|
|
await controller.moveItem(item, targetListId);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(m.checklists.moveFailed)));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _editItem(
|
|
BuildContext context,
|
|
ChecklistsController controller,
|
|
ListItem item,
|
|
) {
|
|
Navigator.of(context).push<bool>(
|
|
MaterialPageRoute(
|
|
builder: (_) => ItemFormView(controller: controller, item: item),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _deleteItem(
|
|
BuildContext context,
|
|
ChecklistsController controller,
|
|
ListItem item,
|
|
) {
|
|
showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text(m.checklists.itemForm.deleteConfirm),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: Text(m.common.cancel),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
child: Text(m.common.delete),
|
|
),
|
|
],
|
|
),
|
|
).then((confirmed) async {
|
|
if (confirmed != true) return;
|
|
try {
|
|
await controller.deleteItem(item);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(m.checklists.itemForm.deleteFailed)),
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SliverReorderableList(
|
|
itemCount: items.length,
|
|
onReorder: (oldIndex, newIndex) {
|
|
if (newIndex > oldIndex) newIndex--;
|
|
controller.reorderItems(items, oldIndex, newIndex);
|
|
},
|
|
itemBuilder: (context, index) {
|
|
final item = items[index];
|
|
return ReorderableDelayedDragStartListener(
|
|
key: ValueKey(item.id),
|
|
index: index,
|
|
child: ChecklistItemTile(
|
|
item: item,
|
|
category: item.categoryId != null
|
|
? controller.categories[item.categoryId]
|
|
: null,
|
|
houseId: controller.houseId,
|
|
onToggle: controller.toggleItem,
|
|
onView: (item) => _viewItem(context, controller, item),
|
|
onEdit: (item) => _editItem(context, controller, item),
|
|
onMove: (item) => _moveItem(context, controller, item),
|
|
onDelete: (item) => _deleteItem(context, controller, item),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|