From 6cdfcdf85c80fb31c20317c46d1c5f46db39c5f2 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 23 Feb 2024 16:13:15 +0100 Subject: [PATCH] add cache to categories list & add autoupdate after editing / creating a category --- lib/backend/room.dart | 85 ++++++++++++++++++++++++-- lib/components/category_chip.dart | 11 +++- lib/components/category_picker.dart | 7 ++- lib/screens/room/categories/edit.dart | 15 +++-- lib/screens/room/items/edit.dart | 5 +- lib/screens/room/items/new.dart | 8 ++- lib/screens/room/pages/categories.dart | 33 ++++++++-- lib/screens/room/pages/list.dart | 23 +++++-- lib/screens/room/products/edit.dart | 5 +- lib/screens/room/products/view.dart | 8 ++- 10 files changed, 171 insertions(+), 29 deletions(-) diff --git a/lib/backend/room.dart b/lib/backend/room.dart index 679bd67..42c85df 100644 --- a/lib/backend/room.dart +++ b/lib/backend/room.dart @@ -363,20 +363,30 @@ class RoomInfo { class RoomCategory { final int? id; - final String name; - final ColorSwatch color; + String name; + ColorSwatch color; + final String room; + final String server; - const RoomCategory( - {required this.id, required this.name, required this.color}); + RoomCategory( + {required this.id, + required this.name, + required this.color, + required this.server, + required this.room}); - factory RoomCategory.fromJSON(dynamic json) { + factory RoomCategory.fromJSON(String server, String room, dynamic json) { return RoomCategory( + server: server, + room: room, id: json['id'], name: json['title'], color: colorFromString(json['color'])); } - factory RoomCategory.other(BuildContext context) { + factory RoomCategory.other(String server, String room, BuildContext context) { return RoomCategory( + server: server, + room: room, id: null, name: AppLocalizations.of(context)!.categoryNameOther, color: Colors.grey); @@ -398,6 +408,69 @@ class RoomCategory { "purple-acc", ].map((txt) => colorFromString(txt)).toList(); } + + // get list of all categories in a given room + static Future> list(String server, String room) async { + final db = Localstore.instance; + final rooms = (await db.collection('categories:$room@$server').get()) ?? {}; + List builder = []; + for (MapEntry entry in rooms.entries) { + try { + builder.add(RoomCategory.fromMap(entry.value)); + } catch (e) { + // skip invalid entries + // NOTE: might want to autodelete them in the future + // although keeping them might be ok, + // in case we ever get a new dataset to fix the current state + } + } + return builder; + } + + // listen to room change + static listen( + String server, String room, Function(Map) cb) async { + final db = Localstore.instance; + final stream = db.collection('categories:$room@$server').stream; + stream.listen(cb); + } + + factory RoomCategory.fromMap(Map map) { + return RoomCategory( + server: map['server'], + room: map['room'], + id: map['id'], + name: map['name'], + color: colorFromString(map['color'])); + } + + Map toMap() { + return { + 'server': server, + 'room': room, + 'id': id, + 'name': name, + 'color': colorIdFromColor(color) + }; + } + + Future toDisk() async { + final db = Localstore.instance; + await db.collection('categories:$room@$server').doc('$id').set(toMap()); + } + + Future removeDisk() async { + final db = Localstore.instance; + await db.collection('categories:$room@$server').doc('$id').delete(); + } + + static Future fromDisk( + {required int id, required String server, required String room}) async { + final db = Localstore.instance; + final raw = + await db.collection('categories:$room@$server').doc('$id').get(); + return RoomCategory.fromMap(raw!); + } } ColorSwatch colorFromString(String text) { diff --git a/lib/components/category_chip.dart b/lib/components/category_chip.dart index e966273..869d1ba 100644 --- a/lib/components/category_chip.dart +++ b/lib/components/category_chip.dart @@ -3,15 +3,20 @@ import 'package:outbag_app/backend/room.dart'; class CategoryChip extends StatelessWidget { final RoomCategory? category; + final String server; + final String room; - const CategoryChip({super.key, this.category}); + const CategoryChip( + {super.key, required this.server, required this.room, this.category}); @override Widget build(BuildContext context) { return ActionChip( avatar: Icon(Icons.square_rounded, - color: category?.color ?? RoomCategory.other(context).color), - label: Text(category?.name ?? RoomCategory.other(context).name), + color: category?.color ?? + RoomCategory.other(server, room, context).color), + label: Text( + category?.name ?? RoomCategory.other(server, room, context).name), ); } } diff --git a/lib/components/category_picker.dart b/lib/components/category_picker.dart index e7c008b..6641a16 100644 --- a/lib/components/category_picker.dart +++ b/lib/components/category_picker.dart @@ -11,9 +11,14 @@ class CategoryPicker extends StatelessWidget { final String? hint; final String? label; + final String server; + final String room; + const CategoryPicker( {super.key, required this.categories, + required this.server, + required this.room, this.selected, this.onSelect, this.hint, @@ -31,7 +36,7 @@ class CategoryPicker extends StatelessWidget { border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.category)), value: selected, - items: [...categories, RoomCategory.other(context)] + items: [...categories, RoomCategory.other(server, room, context)] .map((category) => DropdownMenuItem( value: category.id, child: Row( diff --git a/lib/screens/room/categories/edit.dart b/lib/screens/room/categories/edit.dart index 8ee05c2..13d1696 100644 --- a/lib/screens/room/categories/edit.dart +++ b/lib/screens/room/categories/edit.dart @@ -202,8 +202,13 @@ class _EditCategoryPageState extends State { final id = body['data']['catID']; final cat = RoomCategory( - id: id, name: _ctrName.text, color: _ctrColor); - // TODO: cache category + server: widget.server, + room: widget.tag, + id: id, + name: _ctrName.text, + color: _ctrColor); + // cache category + await cat.toDisk(); // go back router.pop(); @@ -229,11 +234,13 @@ class _EditCategoryPageState extends State { }), onOK: (body) async { final cat = RoomCategory( + server: widget.server, + room: widget.tag, id: widget.id!, name: _ctrName.text, color: _ctrColor); - // TODO: cache category - + // cache category + await cat.toDisk(); // go back router.pop(); return; diff --git a/lib/screens/room/items/edit.dart b/lib/screens/room/items/edit.dart index 331b7c6..fa63a13 100644 --- a/lib/screens/room/items/edit.dart +++ b/lib/screens/room/items/edit.dart @@ -54,7 +54,8 @@ class _EditItemPageState extends State { body: {'room': widget.room, 'server': widget.server}), onOK: (body) async { final resp = body['data'] - .map((raw) => RoomCategory.fromJSON(raw)) + .map((raw) => + RoomCategory.fromJSON(widget.server, widget.room, raw)) .toList(); setState(() { @@ -205,6 +206,8 @@ class _EditItemPageState extends State { }, ), CategoryPicker( + server: widget.server, + room: widget.room, label: AppLocalizations.of(context)! .selectCategoryLabel, hint: AppLocalizations.of(context)! diff --git a/lib/screens/room/items/new.dart b/lib/screens/room/items/new.dart index 47ee511..2f7ad40 100644 --- a/lib/screens/room/items/new.dart +++ b/lib/screens/room/items/new.dart @@ -43,7 +43,8 @@ class _NewItemPageState extends State { body: {'room': widget.room, 'server': widget.server}), onOK: (body) async { final resp = body['data'] - .map((raw) => RoomCategory.fromJSON(raw)) + .map((raw) => + RoomCategory.fromJSON(widget.server, widget.room, raw)) .toList(); setState(() { @@ -262,11 +263,14 @@ class _NewItemPageState extends State { title: Text(e.name), subtitle: Text(e.description), trailing: CategoryChip( + server: widget.server, + room: widget.room, category: categories .where((element) => element.id == e.category) .firstOrNull ?? - RoomCategory.other(context), + RoomCategory.other(widget.server, + widget.room, context), ), onTap: () { // create new item and link it to the product diff --git a/lib/screens/room/pages/categories.dart b/lib/screens/room/pages/categories.dart index 325598f..746c5df 100644 --- a/lib/screens/room/pages/categories.dart +++ b/lib/screens/room/pages/categories.dart @@ -25,6 +25,18 @@ class _RoomCategoriesPageState extends State { void initState() { super.initState(); + // wait for background room category changes + RoomCategory.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "", + (_) async { + try { + final updated = await RoomCategory.list( + widget.room?.serverTag ?? "", widget.room?.id ?? ""); + setState(() { + list = updated; + }); + } catch (_) {} + }); + WidgetsBinding.instance.addPostFrameCallback((_) { fetchCategories(); }); @@ -32,10 +44,18 @@ class _RoomCategoriesPageState extends State { void fetchCategories() async { final user = context.read(); + final scaffmgr = ScaffoldMessenger.of(context); - // TODO: load cached rooms + // load cached categories + final cache = await RoomCategory.list( + widget.room?.serverTag ?? "", widget.room?.id ?? ""); + if (mounted) { + setState(() { + list = cache; + }); + } - doNetworkRequest(ScaffoldMessenger.of(context), + doNetworkRequest(scaffmgr, req: () => postWithCreadentials( credentials: user, target: user.server, @@ -43,8 +63,12 @@ class _RoomCategoriesPageState extends State { body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), onOK: (json) { final resp = json['data'] - .map((raw) => RoomCategory.fromJSON(raw)) + .map((raw) => RoomCategory.fromJSON( + widget.room?.serverTag ?? "", widget.room?.id ?? "", raw)) .toList(); + for (RoomCategory ce in resp) { + ce.toDisk(); + } if (mounted) { setState(() { @@ -204,7 +228,8 @@ class _RoomCategoriesPageState extends State { credentials: user), onOK: (_) async { - // TODO: remove cached category + // remove cached category + item.removeDisk(); fetchCategories(); }, after: () { diff --git a/lib/screens/room/pages/list.dart b/lib/screens/room/pages/list.dart index 87a424f..79289e8 100644 --- a/lib/screens/room/pages/list.dart +++ b/lib/screens/room/pages/list.dart @@ -5,9 +5,7 @@ import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/components/category_chip.dart'; -import 'package:outbag_app/components/category_picker.dart'; import 'package:outbag_app/components/labeled_divider.dart'; -import 'package:outbag_app/components/product_picker.dart'; import 'package:outbag_app/components/value_unit_input.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:provider/provider.dart'; @@ -121,7 +119,8 @@ class _ShoppingListPageState extends State { body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), onOK: (body) async { final resp = body['data'] - .map((raw) => RoomCategory.fromJSON(raw)) + .map((raw) => RoomCategory.fromJSON( + widget.room?.serverTag ?? "", widget.room?.id ?? "", raw)) .toList(); Map map = {}; @@ -208,9 +207,12 @@ class _ShoppingListPageState extends State { itemBuilder: (context, index) { final item = list[index]; final cat = categories[item.category] ?? - RoomCategory.other(context); + RoomCategory.other(widget.room?.serverTag ?? "", + widget.room?.id ?? "", context); return ShoppingListItem( name: item.name, + server: widget.room!.serverTag, + room: widget.room!.id, description: item.description, category: cat, key: Key(item.id.toString()), @@ -259,9 +261,12 @@ class _ShoppingListPageState extends State { itemBuilder: (context, index) { final item = cart[index]; final cat = categories[item.category] ?? - RoomCategory.other(context); + RoomCategory.other(widget.room!.serverTag, + widget.room!.id, context); return ShoppingListItem( + server: widget.room!.serverTag, + room: widget.room!.id, name: item.name, description: item.description, category: cat, @@ -334,12 +339,16 @@ class ShoppingListItem extends StatelessWidget { final Key _key; final Function()? onDismiss; final Function()? onTap; + final String server; + final String room; const ShoppingListItem( {required this.name, required this.category, required this.inCart, required this.description, + required this.server, + required this.room, required key, this.onDismiss, this.onTap}) @@ -372,6 +381,8 @@ class ShoppingListItem extends StatelessWidget { title: Text(name), subtitle: Text(description), trailing: CategoryChip( + server: server, + room: room, category: category, ), onTap: () { @@ -415,6 +426,8 @@ class ShoppingListItemInfo extends StatelessWidget { Text(item.name, style: textTheme.headlineLarge), Text(item.description, style: textTheme.titleMedium), CategoryChip( + server: server, + room: room, category: category, ), Text(Unit.fromId(item.unit).display(context, item.value)) diff --git a/lib/screens/room/products/edit.dart b/lib/screens/room/products/edit.dart index 38d44a9..0e32af2 100644 --- a/lib/screens/room/products/edit.dart +++ b/lib/screens/room/products/edit.dart @@ -49,7 +49,8 @@ class _EditProductPageState extends State { body: {'room': widget.room, 'server': widget.server}), onOK: (body) async { final resp = body['data'] - .map((raw) => RoomCategory.fromJSON(raw)) + .map((raw) => + RoomCategory.fromJSON(widget.server, widget.room, raw)) .toList(); setState(() { @@ -192,6 +193,8 @@ class _EditProductPageState extends State { }, ), CategoryPicker( + server: widget.server, + room: widget.room, label: AppLocalizations.of(context)! .selectCategoryLabel, hint: AppLocalizations.of(context)! diff --git a/lib/screens/room/products/view.dart b/lib/screens/room/products/view.dart index 5aff841..f8901b6 100644 --- a/lib/screens/room/products/view.dart +++ b/lib/screens/room/products/view.dart @@ -66,7 +66,8 @@ class _ViewProductPageState extends State { body: {'room': widget.room, 'server': widget.server}), onOK: (body) async { final resp = body['data'] - .map((raw) => RoomCategory.fromJSON(raw)) + .map((raw) => + RoomCategory.fromJSON(widget.server, widget.room, raw)) .toList(); Map map = {}; @@ -144,7 +145,10 @@ class _ViewProductPageState extends State { Text(product?.description ?? '', style: textTheme.titleMedium), Text(product?.ean ?? ''), - CategoryChip(category: categories[product?.category]), + CategoryChip( + server: widget.server, + room: widget.room, + category: categories[product?.category]), Text(product != null ? Unit.fromId(product!.defaultUnit) .display(context, product!.defaultValue)