5b398eb2ae
- 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)
440 lines
13 KiB
Dart
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()
|
|
});
|
|
},
|
|
)
|
|
]
|
|
: []
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|