add cache to categories list & add autoupdate after editing / creating a
category
This commit is contained in:
parent
13c071b8ca
commit
6cdfcdf85c
10 changed files with 171 additions and 29 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)!
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: () {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)!
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue