diff --git a/lib/backend/room.dart b/lib/backend/room.dart index 323beb8..1916a6c 100644 --- a/lib/backend/room.dart +++ b/lib/backend/room.dart @@ -666,7 +666,7 @@ class RoomProduct { } class RoomItem { - int id; + final int id; int state; String name; String description; @@ -679,9 +679,14 @@ class RoomItem { // may link to a product int? link; + final String server; + final String room; + RoomItem( {required this.id, required this.name, + required this.server, + required this.room, this.description = '', this.state = 0, this.category = -1, @@ -689,10 +694,12 @@ class RoomItem { this.value = '', this.link}); - factory RoomItem.fromJSON(dynamic json) { + factory RoomItem.fromJSON(String server, String room, dynamic json) { return RoomItem( id: json['listItemID'], name: json['title'], + server: server, + room: room, description: json['description'], category: json['listCatID'], state: json['state'], @@ -705,10 +712,84 @@ class RoomItem { return RoomItem( id: id, name: name, + server: server, + room: room, description: description, category: category, unit: unit, value: value, link: link); } + + // 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('items:$room@$server').get()) ?? {}; + List builder = []; + for (MapEntry entry in rooms.entries) { + try { + builder.add(RoomItem.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('items:$room@$server').stream; + stream.listen(cb); + } + + factory RoomItem.fromMap(Map map) { + return RoomItem( + server: map['server'], + room: map['room'], + id: map['id'], + name: map['name'], + description: map['description'], + category: map['category'], + state: map['state'], + unit: map['unit'], + value: map['value'], + link: map['product']); + } + + Map toMap() { + return { + 'server': server, + 'room': room, + 'id': id, + 'name': name, + 'description': description, + 'category': category, + 'state': state, + 'unit': unit, + 'value': value, + 'product': link + }; + } + + Future toDisk() async { + final db = Localstore.instance; + await db.collection('items:$room@$server').doc('$id').set(toMap()); + } + + Future removeDisk() async { + final db = Localstore.instance; + await db.collection('items:$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('items:$room@$server').doc('$id').get(); + return RoomProduct.fromMap(raw!); + } } diff --git a/lib/screens/room/items/edit.dart b/lib/screens/room/items/edit.dart index 15490f5..78eabaa 100644 --- a/lib/screens/room/items/edit.dart +++ b/lib/screens/room/items/edit.dart @@ -41,12 +41,19 @@ class _EditItemPageState extends State { List products = []; RoomItem? item; - void fetchCategories() { + void fetchCategories() async { final user = context.read(); + final scaffmgr = ScaffoldMessenger.of(context); - // TODO: load cached categories first + // load cached categories + final cache = await RoomCategory.list(widget.server, widget.room); + if (mounted) { + setState(() { + categories = cache; + }); + } - doNetworkRequest(ScaffoldMessenger.of(context), + doNetworkRequest(scaffmgr, req: () => postWithCreadentials( credentials: user, target: user.server, @@ -64,12 +71,19 @@ class _EditItemPageState extends State { }); } - void fetchProducts() { + void fetchProducts() async { final user = context.read(); + final scaffmgr = ScaffoldMessenger.of(context); - // TODO: load cached products first + // load cached products first + final cache = await RoomProduct.list(widget.server, widget.room); + if (mounted) { + setState(() { + products = cache; + }); + } - doNetworkRequest(ScaffoldMessenger.of(context), + doNetworkRequest(scaffmgr, req: () => postWithCreadentials( credentials: user, target: user.server, @@ -87,12 +101,19 @@ class _EditItemPageState extends State { }); } - void fetchItem() { + void fetchItem() async { final user = context.read(); + final scaffmgr = ScaffoldMessenger.of(context); - // TODO: load cached item first + // load cached item first + try { + await RoomItem.fromDisk( + id: widget.item, server: widget.server, room: widget.room); + } catch (_) { + // cache miss + } - doNetworkRequest(ScaffoldMessenger.of(context), + doNetworkRequest(scaffmgr, req: () => postWithCreadentials( credentials: user, target: user.server, @@ -103,7 +124,8 @@ class _EditItemPageState extends State { 'listItemID': widget.item }), onOK: (body) async { - final resp = RoomItem.fromJSON(body['data']); + final resp = + RoomItem.fromJSON(widget.server, widget.room, body['data']); setState(() { item = resp; _ctrName.text = resp.name; @@ -121,6 +143,26 @@ class _EditItemPageState extends State { void initState() { super.initState(); + // wait for background room product changes + RoomProduct.listen(widget.server, widget.room, (_) async { + try { + final updated = await RoomProduct.list(widget.server, widget.room); + setState(() { + products = updated; + }); + } catch (_) {} + }); + + // wait for background room category changes + RoomCategory.listen(widget.server, widget.room, (_) async { + try { + final updated = await RoomCategory.list(widget.server, widget.room); + setState(() { + categories = updated; + }); + } catch (_) {} + }); + WidgetsBinding.instance.addPostFrameCallback((_) { fetchCategories(); fetchProducts(); @@ -256,6 +298,20 @@ class _EditItemPageState extends State { 'listProdID': _ctrLink }), onOK: (_) async { + // cache item + final item = RoomItem( + id: widget.item, + server: widget.server, + room: widget.room, + name: _ctrName.text, + state: _ctrState, + description: _ctrDescription.text, + category: _ctrCategory, + unit: _ctrUnit, + value: _ctrValue, + link: _ctrLink); + await item.toDisk(); + nav.pop(); }); }, diff --git a/lib/screens/room/items/new.dart b/lib/screens/room/items/new.dart index a54b292..745bb3f 100644 --- a/lib/screens/room/items/new.dart +++ b/lib/screens/room/items/new.dart @@ -28,14 +28,20 @@ class _NewItemPageState extends State { // data cache List categories = []; List products = []; - RoomItem? item; - void fetchCategories() { + void fetchCategories() async { final user = context.read(); + final scaffmgr = ScaffoldMessenger.of(context); - // TODO: load cached categories first + // load cached categories + final cache = await RoomCategory.list(widget.server, widget.room); + if (mounted) { + setState(() { + categories = cache; + }); + } - doNetworkRequest(ScaffoldMessenger.of(context), + doNetworkRequest(scaffmgr, req: () => postWithCreadentials( credentials: user, target: user.server, @@ -53,12 +59,19 @@ class _NewItemPageState extends State { }); } - void fetchProducts() { + void fetchProducts() async { final user = context.read(); + final scaffmgr = ScaffoldMessenger.of(context); - // TODO: load cached products first + // load cached products first + final cache = await RoomProduct.list(widget.server, widget.room); + if (mounted) { + setState(() { + products = cache; + }); + } - doNetworkRequest(ScaffoldMessenger.of(context), + doNetworkRequest(scaffmgr, req: () => postWithCreadentials( credentials: user, target: user.server, @@ -76,40 +89,33 @@ class _NewItemPageState extends State { }); } - void fetchItem() { - final user = context.read(); - - // TODO: load cached item first - - doNetworkRequest(ScaffoldMessenger.of(context), - req: () => postWithCreadentials( - credentials: user, - target: user.server, - path: 'getItem', - body: { - 'room': widget.room, - 'server': widget.server, - 'listItemID': widget.item - }), - onOK: (body) async { - final resp = RoomItem.fromJSON(body['data']); - setState(() { - item = resp; - }); - }); - } - @override void initState() { super.initState(); + // wait for background room product changes + RoomProduct.listen(widget.server, widget.room, (_) async { + try { + final updated = await RoomProduct.list(widget.server, widget.room); + setState(() { + products = updated; + }); + } catch (_) {} + }); + + // wait for background room category changes + RoomCategory.listen(widget.server, widget.room, (_) async { + try { + final updated = await RoomCategory.list(widget.server, widget.room); + setState(() { + categories = updated; + }); + } catch (_) {} + }); + WidgetsBinding.instance.addPostFrameCallback((_) { fetchCategories(); fetchProducts(); - - if (widget.item != null) { - fetchItem(); - } }); } @@ -138,7 +144,19 @@ class _NewItemPageState extends State { }), onOK: (body) async { final id = body["data"]["listItemID"]; - // TODO cache item + // cache item + final item = RoomItem( + id: id, + room: widget.room, + server: widget.server, + state: 0, + name: name, + description: '', + category: null, + unit: 0, + value: '', + link: productID); + await item.toDisk(); // launch edit item screen router.pushReplacementNamed('edit-item', params: { 'server': widget.server, @@ -170,7 +188,20 @@ class _NewItemPageState extends State { }), onOK: (body) async { final id = body["data"]["listProdID"]; - // TODO: cache product + // cache product + final prod = RoomProduct( + id: id, + name: name, + server: widget.server, + room: widget.room, + description: '', + category: null, + defaultUnit: 0, + defaultValue: '', + ean: '', + parent: null); + await prod.toDisk(); + cb(id); }); } diff --git a/lib/screens/room/pages/list.dart b/lib/screens/room/pages/list.dart index c7dfeb5..67158ff 100644 --- a/lib/screens/room/pages/list.dart +++ b/lib/screens/room/pages/list.dart @@ -28,12 +28,37 @@ class _ShoppingListPageState extends State { Map categories = {}; List products = []; - void fetchItems() { + void fetchItems() async { final user = context.read(); + final scaffmgr = ScaffoldMessenger.of(context); - // TODO: load cached items first + // load cached items first + final cache = await RoomItem.list( + widget.room?.serverTag ?? "", widget.room?.id ?? ""); - doNetworkRequest(ScaffoldMessenger.of(context), + final List l = []; + final List c = []; + + for (RoomItem item in cache) { + if (item.state == 0) { + l.add(item); + } else { + c.add(item); + } + // cache items + await item.toDisk(); + } + + if (mounted) { + setState(() { + list = l; + cart = c; + + sortAll(); + }); + } + + doNetworkRequest(scaffmgr, req: () => postWithCreadentials( credentials: user, target: user.server, @@ -41,7 +66,8 @@ class _ShoppingListPageState extends State { body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), onOK: (body) async { final resp = body['data'] - .map((raw) => RoomItem.fromJSON(raw)) + .map((raw) => RoomItem.fromJSON( + widget.room?.serverTag ?? "", widget.room?.id ?? "", raw)) .toList(); final List l = []; @@ -53,10 +79,10 @@ class _ShoppingListPageState extends State { } else { c.add(item); } + // cache items + await item.toDisk(); } - // TODO: cache items - if (mounted) { setState(() { list = l; @@ -111,17 +137,23 @@ class _ShoppingListPageState extends State { final scaffmgr = ScaffoldMessenger.of(context); // load cached categories - final cache = await RoomCategory.list( + final resp = await RoomCategory.list( widget.room?.serverTag ?? "", widget.room?.id ?? ""); if (mounted) { - Map map = {}; - - for (RoomCategory cat in cache) { - map[cat.id] = cat; + Map map = {}; + Map cat = {}; + for (int i = 0; i < resp.length; i++) { + map[resp[i].id ?? 0] = i; + cat[resp[i].id ?? 0] = resp[i]; + } + + if (mounted) { + setState(() { + weights = map; + categories = cat; + sortAll(); + }); } - setState(() { - categories = map; - }); } doNetworkRequest(scaffmgr, @@ -190,6 +222,34 @@ class _ShoppingListPageState extends State { void initState() { super.initState(); + // wait for background room item changes + RoomItem.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "", + (_) async { + try { + final updated = await RoomItem.list( + widget.room?.serverTag ?? "", widget.room?.id ?? ""); + + final List l = []; + final List c = []; + + for (RoomItem item in updated) { + if (item.state == 0) { + l.add(item); + } else { + c.add(item); + } + } + + if (mounted) { + setState(() { + list = l; + cart = c; + + sortAll(); + }); + } + } catch (_) {} + }); // wait for background room product changes RoomProduct.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "", (_) async { @@ -205,16 +265,22 @@ class _ShoppingListPageState extends State { RoomCategory.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "", (_) async { try { - final updated = await RoomCategory.list( + final resp = await RoomCategory.list( widget.room?.serverTag ?? "", widget.room?.id ?? ""); - Map map = {}; - - for (RoomCategory cat in updated) { - map[cat.id] = cat; + Map map = {}; + Map cat = {}; + for (int i = 0; i < resp.length; i++) { + map[resp[i].id ?? 0] = i; + cat[resp[i].id ?? 0] = resp[i]; + } + + if (mounted) { + setState(() { + weights = map; + categories = cat; + sortAll(); + }); } - setState(() { - categories = map; - }); } catch (_) {} }); @@ -518,6 +584,9 @@ class ShoppingListItemInfo extends StatelessWidget { 'id': room, 'item': item.id.toString() }); + + final navInner = Navigator.of(context); + navInner.pop(); }, ), ListTile( @@ -567,7 +636,8 @@ class ShoppingListItemInfo extends StatelessWidget { }, credentials: user), onOK: (_) async { - // TODO: remove cached item + // remove cached item + await item.removeDisk(); }, after: () { // close popup @@ -587,9 +657,9 @@ class ShoppingListItemInfo extends StatelessWidget { ListTile( title: Text(item.state == 0 ? AppLocalizations.of(context)!.moveItemToCartTitle - : AppLocalizations.of(context)!.moveItemToCartSubtitle), + : AppLocalizations.of(context)!.removeItemFromCartTitle), subtitle: Text(item.state == 0 - ? AppLocalizations.of(context)!.removeItemFromCartTitle + ? AppLocalizations.of(context)!.moveItemToCartSubtitle : AppLocalizations.of(context)!.removeItemFromCartSubtitle), onTap: () { // flip state @@ -605,7 +675,12 @@ class ShoppingListItemInfo extends StatelessWidget { 'server': server, 'listItemID': item.id, 'state': item.state - })); + }), + onOK: (_) async { + final navInner = Navigator.of(context); + await item.toDisk(); + navInner.pop(); + }); }) ], ), diff --git a/lib/screens/room/products/edit.dart b/lib/screens/room/products/edit.dart index 0f88259..a8fe42c 100644 --- a/lib/screens/room/products/edit.dart +++ b/lib/screens/room/products/edit.dart @@ -118,6 +118,16 @@ class _EditProductPageState extends State { void initState() { super.initState(); + // wait for background room category changes + RoomCategory.listen(widget.server, widget.room, (_) async { + try { + final updated = await RoomCategory.list(widget.server, widget.room); + setState(() { + categories = updated; + }); + } catch (_) {} + }); + WidgetsBinding.instance.addPostFrameCallback((_) { fetchCategories(); fetchProducts();