add cache to categories list & add autoupdate after editing / creating a

category
This commit is contained in:
Jakob Meier 2024-02-23 16:13:15 +01:00
parent 13c071b8ca
commit 6cdfcdf85c
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
10 changed files with 171 additions and 29 deletions

View file

@ -363,20 +363,30 @@ class RoomInfo {
class RoomCategory { class RoomCategory {
final int? id; final int? id;
final String name; String name;
final ColorSwatch<int> color; ColorSwatch<int> color;
final String room;
final String server;
const RoomCategory( RoomCategory(
{required this.id, required this.name, required this.color}); {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( return RoomCategory(
server: server,
room: room,
id: json['id'], id: json['id'],
name: json['title'], name: json['title'],
color: colorFromString(json['color'])); color: colorFromString(json['color']));
} }
factory RoomCategory.other(BuildContext context) { factory RoomCategory.other(String server, String room, BuildContext context) {
return RoomCategory( return RoomCategory(
server: server,
room: room,
id: null, id: null,
name: AppLocalizations.of(context)!.categoryNameOther, name: AppLocalizations.of(context)!.categoryNameOther,
color: Colors.grey); color: Colors.grey);
@ -398,6 +408,69 @@ class RoomCategory {
"purple-acc", "purple-acc",
].map((txt) => colorFromString(txt)).toList(); ].map((txt) => colorFromString(txt)).toList();
} }
// get list of all categories in a given room
static Future<List<RoomCategory>> list(String server, String room) async {
final db = Localstore.instance;
final rooms = (await db.collection('categories:$room@$server').get()) ?? {};
List<RoomCategory> 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<String, dynamic>) cb) async {
final db = Localstore.instance;
final stream = db.collection('categories:$room@$server').stream;
stream.listen(cb);
}
factory RoomCategory.fromMap(Map<String, dynamic> map) {
return RoomCategory(
server: map['server'],
room: map['room'],
id: map['id'],
name: map['name'],
color: colorFromString(map['color']));
}
Map<String, dynamic> toMap() {
return {
'server': server,
'room': room,
'id': id,
'name': name,
'color': colorIdFromColor(color)
};
}
Future<void> toDisk() async {
final db = Localstore.instance;
await db.collection('categories:$room@$server').doc('$id').set(toMap());
}
Future<void> removeDisk() async {
final db = Localstore.instance;
await db.collection('categories:$room@$server').doc('$id').delete();
}
static Future<RoomCategory> 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<int> colorFromString(String text) { ColorSwatch<int> colorFromString(String text) {

View file

@ -3,15 +3,20 @@ import 'package:outbag_app/backend/room.dart';
class CategoryChip extends StatelessWidget { class CategoryChip extends StatelessWidget {
final RoomCategory? category; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActionChip( return ActionChip(
avatar: Icon(Icons.square_rounded, avatar: Icon(Icons.square_rounded,
color: category?.color ?? RoomCategory.other(context).color), color: category?.color ??
label: Text(category?.name ?? RoomCategory.other(context).name), RoomCategory.other(server, room, context).color),
label: Text(
category?.name ?? RoomCategory.other(server, room, context).name),
); );
} }
} }

View file

@ -11,9 +11,14 @@ class CategoryPicker extends StatelessWidget {
final String? hint; final String? hint;
final String? label; final String? label;
final String server;
final String room;
const CategoryPicker( const CategoryPicker(
{super.key, {super.key,
required this.categories, required this.categories,
required this.server,
required this.room,
this.selected, this.selected,
this.onSelect, this.onSelect,
this.hint, this.hint,
@ -31,7 +36,7 @@ class CategoryPicker extends StatelessWidget {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.category)), prefixIcon: const Icon(Icons.category)),
value: selected, value: selected,
items: [...categories, RoomCategory.other(context)] items: [...categories, RoomCategory.other(server, room, context)]
.map((category) => DropdownMenuItem<int?>( .map((category) => DropdownMenuItem<int?>(
value: category.id, value: category.id,
child: Row( child: Row(

View file

@ -202,8 +202,13 @@ class _EditCategoryPageState extends State<EditCategoryPage> {
final id = body['data']['catID']; final id = body['data']['catID'];
final cat = RoomCategory( final cat = RoomCategory(
id: id, name: _ctrName.text, color: _ctrColor); server: widget.server,
// TODO: cache category room: widget.tag,
id: id,
name: _ctrName.text,
color: _ctrColor);
// cache category
await cat.toDisk();
// go back // go back
router.pop(); router.pop();
@ -229,11 +234,13 @@ class _EditCategoryPageState extends State<EditCategoryPage> {
}), }),
onOK: (body) async { onOK: (body) async {
final cat = RoomCategory( final cat = RoomCategory(
server: widget.server,
room: widget.tag,
id: widget.id!, id: widget.id!,
name: _ctrName.text, name: _ctrName.text,
color: _ctrColor); color: _ctrColor);
// TODO: cache category // cache category
await cat.toDisk();
// go back // go back
router.pop(); router.pop();
return; return;

View file

@ -54,7 +54,8 @@ class _EditItemPageState extends State<EditItemPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
setState(() { setState(() {
@ -205,6 +206,8 @@ class _EditItemPageState extends State<EditItemPage> {
}, },
), ),
CategoryPicker( CategoryPicker(
server: widget.server,
room: widget.room,
label: AppLocalizations.of(context)! label: AppLocalizations.of(context)!
.selectCategoryLabel, .selectCategoryLabel,
hint: AppLocalizations.of(context)! hint: AppLocalizations.of(context)!

View file

@ -43,7 +43,8 @@ class _NewItemPageState extends State<NewItemPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
setState(() { setState(() {
@ -262,11 +263,14 @@ class _NewItemPageState extends State<NewItemPage> {
title: Text(e.name), title: Text(e.name),
subtitle: Text(e.description), subtitle: Text(e.description),
trailing: CategoryChip( trailing: CategoryChip(
server: widget.server,
room: widget.room,
category: categories category: categories
.where((element) => .where((element) =>
element.id == e.category) element.id == e.category)
.firstOrNull ?? .firstOrNull ??
RoomCategory.other(context), RoomCategory.other(widget.server,
widget.room, context),
), ),
onTap: () { onTap: () {
// create new item and link it to the product // create new item and link it to the product

View file

@ -25,6 +25,18 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
void initState() { void initState() {
super.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((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories(); fetchCategories();
}); });
@ -32,10 +44,18 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
void fetchCategories() async { void fetchCategories() async {
final user = context.read<User>(); final user = context.read<User>();
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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -43,8 +63,12 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (json) { onOK: (json) {
final resp = json['data'] final resp = json['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) => RoomCategory.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList(); .toList();
for (RoomCategory ce in resp) {
ce.toDisk();
}
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -204,7 +228,8 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
credentials: credentials:
user), user),
onOK: (_) async { onOK: (_) async {
// TODO: remove cached category // remove cached category
item.removeDisk();
fetchCategories(); fetchCategories();
}, },
after: () { after: () {

View file

@ -5,9 +5,7 @@ import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/components/category_chip.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/labeled_divider.dart';
import 'package:outbag_app/components/product_picker.dart';
import 'package:outbag_app/components/value_unit_input.dart'; import 'package:outbag_app/components/value_unit_input.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -121,7 +119,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) => RoomCategory.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList(); .toList();
Map<int, int> map = {}; Map<int, int> map = {};
@ -208,9 +207,12 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = list[index]; final item = list[index];
final cat = categories[item.category] ?? final cat = categories[item.category] ??
RoomCategory.other(context); RoomCategory.other(widget.room?.serverTag ?? "",
widget.room?.id ?? "", context);
return ShoppingListItem( return ShoppingListItem(
name: item.name, name: item.name,
server: widget.room!.serverTag,
room: widget.room!.id,
description: item.description, description: item.description,
category: cat, category: cat,
key: Key(item.id.toString()), key: Key(item.id.toString()),
@ -259,9 +261,12 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = cart[index]; final item = cart[index];
final cat = categories[item.category] ?? final cat = categories[item.category] ??
RoomCategory.other(context); RoomCategory.other(widget.room!.serverTag,
widget.room!.id, context);
return ShoppingListItem( return ShoppingListItem(
server: widget.room!.serverTag,
room: widget.room!.id,
name: item.name, name: item.name,
description: item.description, description: item.description,
category: cat, category: cat,
@ -334,12 +339,16 @@ class ShoppingListItem extends StatelessWidget {
final Key _key; final Key _key;
final Function()? onDismiss; final Function()? onDismiss;
final Function()? onTap; final Function()? onTap;
final String server;
final String room;
const ShoppingListItem( const ShoppingListItem(
{required this.name, {required this.name,
required this.category, required this.category,
required this.inCart, required this.inCart,
required this.description, required this.description,
required this.server,
required this.room,
required key, required key,
this.onDismiss, this.onDismiss,
this.onTap}) this.onTap})
@ -372,6 +381,8 @@ class ShoppingListItem extends StatelessWidget {
title: Text(name), title: Text(name),
subtitle: Text(description), subtitle: Text(description),
trailing: CategoryChip( trailing: CategoryChip(
server: server,
room: room,
category: category, category: category,
), ),
onTap: () { onTap: () {
@ -415,6 +426,8 @@ class ShoppingListItemInfo extends StatelessWidget {
Text(item.name, style: textTheme.headlineLarge), Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium), Text(item.description, style: textTheme.titleMedium),
CategoryChip( CategoryChip(
server: server,
room: room,
category: category, category: category,
), ),
Text(Unit.fromId(item.unit).display(context, item.value)) Text(Unit.fromId(item.unit).display(context, item.value))

View file

@ -49,7 +49,8 @@ class _EditProductPageState extends State<EditProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
setState(() { setState(() {
@ -192,6 +193,8 @@ class _EditProductPageState extends State<EditProductPage> {
}, },
), ),
CategoryPicker( CategoryPicker(
server: widget.server,
room: widget.room,
label: AppLocalizations.of(context)! label: AppLocalizations.of(context)!
.selectCategoryLabel, .selectCategoryLabel,
hint: AppLocalizations.of(context)! hint: AppLocalizations.of(context)!

View file

@ -66,7 +66,8 @@ class _ViewProductPageState extends State<ViewProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
Map<int?, RoomCategory> map = {}; Map<int?, RoomCategory> map = {};
@ -144,7 +145,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
Text(product?.description ?? '', Text(product?.description ?? '',
style: textTheme.titleMedium), style: textTheme.titleMedium),
Text(product?.ean ?? ''), Text(product?.ean ?? ''),
CategoryChip(category: categories[product?.category]), CategoryChip(
server: widget.server,
room: widget.room,
category: categories[product?.category]),
Text(product != null Text(product != null
? Unit.fromId(product!.defaultUnit) ? Unit.fromId(product!.defaultUnit)
.display(context, product!.defaultValue) .display(context, product!.defaultValue)