actions-test/lib/screens/room/pages/list.dart
Jakob Meier 5b398eb2ae
Bug fixes (see description)
- Fixed bug where switching room pages
  (list,products,categories,about),
  would result an unknown error,
  due to setState being called on a widget that isn't mounted.
  This was solved by surrounding the setState function,
  with a condition to check if the widget is mounted

- Fixed bug where room members weren't recognized as admins
  This was caused by a typedifference between the server and the app
  (The server now returns booleans,
  where as before a ==1 comparison was
  needed)
- Fixed bug where successfully closing the admin/kick member dialog,
  would crash the application.
  This was caused by popping the same context twice.
  We are now using two navigator (the outer and the inner one)
2023-04-05 08:54:48 +02:00

440 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:outbag_app/backend/permissions.dart';
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/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ShoppingListPage extends StatefulWidget {
final RoomInfo? info;
final Room? room;
const ShoppingListPage(this.room, this.info, {super.key});
@override
State<StatefulWidget> createState() => _ShoppingListPageState();
}
class _ShoppingListPageState extends State<ShoppingListPage> {
List<RoomItem> list = [];
List<RoomItem> cart = [];
Map<int, int> weights = {};
List<RoomCategory> categories = [];
List<RoomProduct> products = [];
void fetchItems() {
final user = context.read<User>();
// TODO: load cached items first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getItems',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomItem>((raw) => RoomItem.fromJSON(raw))
.toList();
final List<RoomItem> l = [];
final List<RoomItem> c = [];
for (RoomItem item in resp) {
if (item.state == 0) {
l.add(item);
} else {
c.add(item);
}
}
// TODO: cache items
if (mounted) {
setState(() {
list = l;
cart = c;
sortAll();
});
}
});
}
void sortAll() {
for (List<RoomItem> input in [list, cart]) {
setState(() {
input.sort((a, b) {
if (a.category == b.category) {
return 0;
}
if (a.category == null) {
// b should be below
return -1;
}
if (b.category == null) {
// a should be below
return 1;
}
final weightA = weights[a.category];
final weightB = weights[b.category];
// both could be null now,
// so we have to check agein
if (weightA == weightB) {
return 0;
}
if (weightA == null) {
// b should be below
return -1;
}
if (weightB == null) {
// a should be below
return 1;
}
return weightA.compareTo(weightB);
});
});
}
}
void fetchCategories() {
final user = context.read<User>();
// TODO: load cached categories first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
Map<int, int> map = {};
for (int i = 0; i < resp.length; i++) {
map[resp[i].id] = i;
}
if (mounted) {
setState(() {
weights = map;
categories = resp;
sortAll();
});
}
});
}
void fetchProducts() {
final user = context.read<User>();
// TODO: load cached products first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
if (mounted) {
setState(() {
products = resp;
});
}
});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchItems();
fetchCategories();
fetchProducts();
});
}
void changeItemState(RoomItem item) {
final user = context.read<User>();
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'changeItemState',
body: {
'room': widget.room?.id,
'server': widget.room?.serverTag,
'listItemID': item.id,
'state': item.state
}));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(children: [
LabeledDivider(AppLocalizations.of(context)!.shoppingList),
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
return ShoppingListItem(
name: item.name,
description: item.description,
category: (item.category != null)
? categories[item.category!]
: RoomCategory.other(context),
key: Key(item.id.toString()),
inCart: item.state != 0,
onDismiss: () {
// move to cart
item.state = 1;
setState(() {
list.removeAt(index);
cart.add(item);
sortAll();
});
// network request
// NOTE: for now we do not care if it is successfull,
// because the shopping cart is supposed to work even
// without network
changeItemState(item);
},
onTap: () {
// TODO: show modal bottom sheet
// containing
// - item info
// - option to view linked product (if available)
// - edit item (if allowed)
// - delete item (if allowed)
// - move to/from shopping cart?
showModalBottomSheet(
context: context,
builder: (context) => ShoppingListItemInfo(
products: products,
categories: categories,
item: item,
room: widget.room!.id,
server: widget.room!.serverTag));
});
},
),
LabeledDivider(AppLocalizations.of(context)!.shoppingCart),
ListView.builder(
itemCount: cart.length,
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
final item = cart[index];
return ShoppingListItem(
name: item.name,
description: item.description,
category: (item.category != null)
? categories[item.category!]
: RoomCategory.other(context),
key: Key(item.id.toString()),
inCart: item.state != 0,
onDismiss: () {
// move back to list
item.state = 0;
setState(() {
cart.removeAt(index);
list.add(item);
sortAll();
});
// network request
// NOTE: for now we do not care if it is successfull,
// because the shopping cart is supposed to work even
// without network
changeItemState(item);
},
onTap: () {
// TODO: show modal bottom sheet
// containing
// - item info
// - option to view linked product (if available)
// - edit item (if allowed)
// - delete item (if allowed)
// - move to/from shopping cart?
showModalBottomSheet(
context: context,
builder: (context) => ShoppingListItemInfo(
products: products,
categories: categories,
item: item,
room: widget.room!.id,
server: widget.room!.serverTag));
});
},
)
]),
floatingActionButton: ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & RoomPermission.editRoomContent !=
0))
? FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text(AppLocalizations.of(context)!.newItemShort),
tooltip: AppLocalizations.of(context)!.newItemLong,
onPressed: () {
// show new category popup
context.pushNamed('new-item', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
);
}
}
class ShoppingListItem extends StatelessWidget {
String name;
RoomCategory category;
String description;
bool inCart;
final Key _key;
Function()? onDismiss;
Function()? onTap;
ShoppingListItem(
{required this.name,
required this.category,
required this.inCart,
required this.description,
required key,
this.onDismiss,
this.onTap})
: _key = key;
@override
Widget build(BuildContext context) {
return Dismissible(
key: Key('item-$_key'),
dismissThresholds: const {
// NOTE: value might need updating
// maybe we could calculate this using the screen width
DismissDirection.horizontal: 500.0,
},
confirmDismiss: (_) async {
if (onDismiss != null) {
onDismiss!();
}
// keep item in list
// NOTE: might want to set this to true/variable
// if in-shopping-cart items have a second screen
return true;
},
background:
Icon(!inCart ? Icons.shopping_cart : Icons.remove_shopping_cart),
child: ListTile(
enabled: !inCart,
title: Text(name),
subtitle: Text(description),
trailing: CategoryChip(
category: category,
),
onTap: () {
if (onTap != null) {
onTap!();
}
},
),
);
}
}
class ShoppingListItemInfo extends StatelessWidget {
RoomItem item;
String server;
String room;
List<RoomCategory> categories = [];
List<RoomProduct> products = [];
ShoppingListItemInfo(
{required this.item,
required this.server,
required this.room,
required this.categories,
required this.products});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return BottomSheet(
onClosing: () {},
builder: (context) => Column(
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Center(
child: Column(children: [
Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium),
CategoryPicker(
label: AppLocalizations.of(context)!.selectCategoryLabel,
categories: categories,
selected: item.category,
enabled: false),
ProductPicker(
label:
AppLocalizations.of(context)!.selectLinkedProductLabel,
help: AppLocalizations.of(context)!.selectLinkedProductHelp,
products: products,
selected: item.link,
enabled: false)
// TODO: show more info
]))),
...(item.link != null)
? [
ListTile(
title: Text(AppLocalizations.of(context)!
.itemShowLinkedProductTitle),
subtitle: Text(AppLocalizations.of(context)!
.itemShowLinkedProductSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// launch "view-product" page for specific product
context.pushNamed('view-product', params: {
'server': server,
'id': room,
'product': item.id.toString()
});
},
)
]
: []
],
),
);
}
}