Compare commits

..

10 commits

Author SHA1 Message Date
80ab81f1f1
apk build action
All checks were successful
/ build (push) Successful in 3m48s
2024-02-26 12:46:02 +01:00
Jakob Meier
7174a03cf2
cache itemss for items list and edit item screen
BUG: when deleting an item the item list won't update
2024-02-23 20:44:42 +01:00
Jakob Meier
9ff6d97c90
cache products for item and product list and product view screen
BUG: when deleting a product the product list won't update
2024-02-23 20:06:49 +01:00
Jakob Meier
6cdfcdf85c
add cache to categories list & add autoupdate after editing / creating a
category
2024-02-23 16:13:15 +01:00
Jakob Meier
13c071b8ca
remove item&product confirmation dialog
Because there currently is no way to auto-refresh the lists it will
appear as if nothing changed.
Switching to a different tab and back again will update the list though
2024-02-23 10:16:57 +01:00
Jakob Meier
384fbb0573
separate new item screen
The edit item screen might be overwhelming at first,
and if you only want to add simple items (by name) to the list,
it is way easier to simply type the name and click create to create a
simple item.
After creating the item the user will be redirected (history
replacement) to the edit screen, but clicking back will bring them back
to the list.
This screen also makes linking products easier and allows the user to
create new products if they notice they are using the same item multiple
times or can't be bothered to switch to the products tab
2024-02-22 20:36:59 +01:00
Jakob Meier
b013964615
Fixed hooks 2024-02-22 15:32:55 +01:00
Jakob Meier
a897d4c4af
Added scrollbar to room info bottomsheet 2023-12-22 20:45:12 +01:00
Jakob Meier
2fa9486db3
Added pre-commit hooks 2023-12-22 20:45:12 +01:00
Jakob Meier
50b6d038c3
Updated yaml files 2023-12-22 20:45:12 +01:00
23 changed files with 2202 additions and 935 deletions

View file

@ -0,0 +1,16 @@
on:
push:
tags:
- v*
jobs:
build:
runs-on: default
container:
image: ghcr.io/cirruslabs/flutter:stable
steps:
- run: apt-get update && apt-get install nodejs -y
- name: Checkout Repository
uses: actions/checkout@v3
- run: flutter --version
- run: flutter build apk --release

View file

@ -16,3 +16,19 @@ This app uses /l10n/ according to the official flutter
We use [weblate](https://translate.codeberg.org/engage/outbag/) to manage translations.
[![Translation status](https://translate.codeberg.org/widget/outbag/outbag-app/multi-auto.svg)](https://translate.codeberg.org/engage/outbag/)
## Contributing
To keep the code formatted properly,
we use git hooks to format files before committing.
You can easily add git-hooks using the following command:
``` sh
git config core.hooksPath ./hooks
```
Afterwards the `pre-commit` hook will automatically be run,
when you commit your changes:
``` sh
git add # some files
git commit
```

12
hooks/pre-commit Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
# adapted from https://prettier.io/docs/en/precommit.html#option-5-shell-script
FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g' | grep '.dart$')
[ -z "$FILES" ] && exit 0
# Format all selected files
echo "$FILES" | xargs dart format
# Add back the formatted files to staging
echo "$FILES" | xargs git add
exit 0

View file

@ -1,3 +1,4 @@
---
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

View file

@ -363,20 +363,30 @@ class RoomInfo {
class RoomCategory {
final int? id;
final String name;
final ColorSwatch<int> color;
String name;
ColorSwatch<int> color;
final String room;
final String server;
const RoomCategory(
{required this.id, required this.name, required this.color});
RoomCategory(
{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(
server: server,
room: room,
id: json['id'],
name: json['title'],
color: colorFromString(json['color']));
}
factory RoomCategory.other(BuildContext context) {
factory RoomCategory.other(String server, String room, BuildContext context) {
return RoomCategory(
server: server,
room: room,
id: null,
name: AppLocalizations.of(context)!.categoryNameOther,
color: Colors.grey);
@ -398,6 +408,69 @@ class RoomCategory {
"purple-acc",
].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) {
@ -474,7 +547,7 @@ String colorIdFromColor(ColorSwatch<int> color) {
}
class RoomProduct {
int id;
final int id;
String name;
String description;
// category ID
@ -490,9 +563,14 @@ class RoomProduct {
// parent product ID
int? parent;
final String server;
final String room;
RoomProduct(
{required this.id,
required this.name,
required this.server,
required this.room,
this.description = '',
this.category = -1,
this.defaultUnit = 0,
@ -500,8 +578,10 @@ class RoomProduct {
this.ean,
this.parent});
factory RoomProduct.fromJSON(dynamic json) {
factory RoomProduct.fromJSON(String server, String room, dynamic json) {
return RoomProduct(
server: server,
room: room,
id: json['listProdID'],
name: json['title'],
description: json['description'],
@ -511,10 +591,82 @@ class RoomProduct {
ean: json['ean'],
parent: json['parent']);
}
// get list of all categories in a given room
static Future<List<RoomProduct>> list(String server, String room) async {
final db = Localstore.instance;
final rooms = (await db.collection('products:$room@$server').get()) ?? {};
List<RoomProduct> builder = [];
for (MapEntry entry in rooms.entries) {
try {
builder.add(RoomProduct.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('products:$room@$server').stream;
stream.listen(cb);
}
factory RoomProduct.fromMap(Map<String, dynamic> map) {
return RoomProduct(
server: map['server'],
room: map['room'],
id: map['id'],
name: map['name'],
description: map['description'],
category: map['category'],
defaultUnit: map['default_unit'],
defaultValue: map['default_value'],
ean: map['ean'],
parent: map['parent']);
}
Map<String, dynamic> toMap() {
return {
'server': server,
'room': room,
'id': id,
'name': name,
'description': description,
'category': category,
'default_unit': defaultUnit,
'default_value': defaultValue,
'ean': ean,
'parent': parent
};
}
Future<void> toDisk() async {
final db = Localstore.instance;
await db.collection('products:$room@$server').doc('$id').set(toMap());
}
Future<void> removeDisk() async {
final db = Localstore.instance;
await db.collection('products:$room@$server').doc('$id').delete();
}
static Future<RoomProduct> fromDisk(
{required int id, required String server, required String room}) async {
final db = Localstore.instance;
final raw = await db.collection('products:$room@$server').doc('$id').get();
return RoomProduct.fromMap(raw!);
}
}
class RoomItem {
int id;
final int id;
int state;
String name;
String description;
@ -527,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,
@ -537,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'],
@ -553,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<RoomItem>> list(String server, String room) async {
final db = Localstore.instance;
final rooms = (await db.collection('items:$room@$server').get()) ?? {};
List<RoomItem> 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<String, dynamic>) cb) async {
final db = Localstore.instance;
final stream = db.collection('items:$room@$server').stream;
stream.listen(cb);
}
factory RoomItem.fromMap(Map<String, dynamic> 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<String, dynamic> toMap() {
return {
'server': server,
'room': room,
'id': id,
'name': name,
'description': description,
'category': category,
'state': state,
'unit': unit,
'value': value,
'product': link
};
}
Future<void> toDisk() async {
final db = Localstore.instance;
await db.collection('items:$room@$server').doc('$id').set(toMap());
}
Future<void> removeDisk() async {
final db = Localstore.instance;
await db.collection('items:$room@$server').doc('$id').delete();
}
static Future<RoomProduct> 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!);
}
}

View file

@ -3,15 +3,20 @@ import 'package:outbag_app/backend/room.dart';
class CategoryChip extends StatelessWidget {
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
Widget build(BuildContext context) {
return ActionChip(
avatar: Icon(Icons.square_rounded,
color: category?.color ?? RoomCategory.other(context).color),
label: Text(category?.name ?? RoomCategory.other(context).name),
color: category?.color ??
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? label;
final String server;
final String room;
const CategoryPicker(
{super.key,
required this.categories,
required this.server,
required this.room,
this.selected,
this.onSelect,
this.hint,
@ -31,7 +36,7 @@ class CategoryPicker extends StatelessWidget {
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.category)),
value: selected,
items: [...categories, RoomCategory.other(context)]
items: [...categories, RoomCategory.other(server, room, context)]
.map((category) => DropdownMenuItem<int?>(
value: category.id,
child: Row(

View file

@ -424,5 +424,17 @@
"moveItemToCartTitle": "Move to Cart",
"moveItemToCartSubtitle": "Mark item as in-cart, so others know, you bought it",
"removeItemFromCartTitle": "Remove from Cart",
"removeItemFromCartSubtitle": "Remove item from shopping cart, so others know, that you still need it"
"removeItemFromCartSubtitle": "Remove item from shopping cart, so others know, that you still need it",
"newItemQueryEmpty": "Type to show quick access buttons",
"newItemQueryHint": "Type item or product name",
"newItemQuickAccessPrefix": "Create:",
"newItemQuickProduct": "product: {text}",
"deleteItem": "Delete Item",
"deleteItemConfirm": "Do you really want to remove the item named {item}",
"deleteProductTitle": "Delete Product",
"deleteProductSubtitle": "Remove the product from list of known products",
"deleteProduct": "Delete Product",
"deleteProductConfirm": "DO you really want to remove the product named {product}"
}

View file

@ -6,6 +6,7 @@ import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/screens/room/categories/edit.dart';
import 'package:outbag_app/screens/room/items/edit.dart';
import 'package:outbag_app/screens/room/items/new.dart';
import 'package:outbag_app/screens/room/products/edit.dart';
import 'package:outbag_app/screens/room/products/view.dart';
@ -306,7 +307,7 @@ class _OutbagAppState extends State {
GoRoute(
name: 'new-item',
path: 'new-item',
builder: (context, state) => EditItemPage(
builder: (context, state) => NewItemPage(
server:
state.params['server'] ?? '',
room: state.params['id'] ?? '',

View file

@ -123,7 +123,12 @@ class _HomePageState extends State<HomePage> {
)
],
),
body: ListView.builder(
body: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView.builder(
itemCount: rooms.length,
itemBuilder: (ctx, i) {
final room = rooms[i];
@ -134,27 +139,32 @@ class _HomePageState extends State<HomePage> {
child: InkWell(
onTap: () {
// open room
context.goNamed('room',
params: {'server': room.serverTag, 'id': room.id});
context.goNamed('room', params: {
'server': room.serverTag,
'id': room.id
});
},
onLongPress: () {
// open bottom sheet
// NOTE: feature yet to be confirmed
},
child: Container(
padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
padding:
const EdgeInsets.fromLTRB(10, 5, 5, 10),
child: ListTile(
title: Text(room.name),
visualDensity: const VisualDensity(vertical: 3),
visualDensity:
const VisualDensity(vertical: 3),
subtitle: Text(room.description),
leading: AspectRatio(
aspectRatio: 1 / 1,
child: SvgPicture.asset("${room.icon?.img}"),
child:
SvgPicture.asset("${room.icon?.img}"),
),
hoverColor: Colors.transparent,
))));
},
),
)))),
floatingActionButton: FloatingActionButton.extended(
label: Text(AppLocalizations.of(context)!.addRoom),
icon: const Icon(Icons.add),

View file

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

View file

@ -14,10 +14,13 @@ class EditItemPage extends StatefulWidget {
final String room;
final String server;
final int? item;
final int item;
const EditItemPage(
{super.key, required this.room, required this.server, this.item});
{super.key,
required this.room,
required this.server,
required this.item});
@override
State<StatefulWidget> createState() => _EditItemPageState();
@ -31,18 +34,26 @@ class _EditItemPageState extends State<EditItemPage> {
int _ctrUnit = 0;
String _ctrValue = '';
int? _ctrLink;
int _ctrState = 0;
// data cache
List<RoomCategory> categories = [];
List<RoomProduct> products = [];
RoomItem? item;
void fetchCategories() {
void fetchCategories() async {
final user = context.read<User>();
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,
@ -50,7 +61,8 @@ class _EditItemPageState extends State<EditItemPage> {
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList();
setState(() {
@ -59,12 +71,19 @@ class _EditItemPageState extends State<EditItemPage> {
});
}
void fetchProducts() {
void fetchProducts() async {
final user = context.read<User>();
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,
@ -72,7 +91,8 @@ class _EditItemPageState extends State<EditItemPage> {
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList();
setState(() {
@ -81,12 +101,19 @@ class _EditItemPageState extends State<EditItemPage> {
});
}
void fetchItem() {
void fetchItem() async {
final user = context.read<User>();
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,
@ -97,9 +124,17 @@ class _EditItemPageState extends State<EditItemPage> {
'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;
_ctrDescription.text = resp.description;
_ctrValue = resp.value;
_ctrCategory = resp.category;
_ctrLink = resp.link;
_ctrUnit = resp.unit;
_ctrState = resp.state;
});
});
}
@ -108,13 +143,31 @@ class _EditItemPageState extends State<EditItemPage> {
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();
}
});
}
@ -122,16 +175,14 @@ class _EditItemPageState extends State<EditItemPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text((widget.item == null)
? AppLocalizations.of(context)!.createItem
: AppLocalizations.of(context)!.editItem),
title: Text(AppLocalizations.of(context)!.editItem),
),
body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
@ -198,6 +249,8 @@ class _EditItemPageState extends State<EditItemPage> {
},
),
CategoryPicker(
server: widget.server,
room: widget.room,
label: AppLocalizations.of(context)!
.selectCategoryLabel,
hint: AppLocalizations.of(context)!
@ -227,27 +280,6 @@ class _EditItemPageState extends State<EditItemPage> {
final user = context.read<User>();
if (widget.item == null) {
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'addItem',
body: {
'room': widget.room,
'server': widget.server,
'state': 0,
'title': _ctrName.text,
'description': _ctrDescription.text,
'listCatID': _ctrCategory,
'unit': _ctrUnit,
'value': _ctrValue,
'listProdID': _ctrLink
}),
onOK: (_) async {
nav.pop();
});
} else {
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
credentials: user,
@ -258,21 +290,33 @@ class _EditItemPageState extends State<EditItemPage> {
'room': widget.room,
'server': widget.server,
'title': _ctrName.text,
'state': _ctrState,
'description': _ctrDescription.text,
'listCatID': _ctrCategory,
'defUnit': _ctrUnit,
'defValue': _ctrValue,
'unit': _ctrUnit,
'value': _ctrValue,
'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();
});
}
},
label: Text(widget.item != null
? AppLocalizations.of(context)!.editItemShort
: AppLocalizations.of(context)!.createItemShort),
icon: Icon(widget.item != null ? Icons.edit : Icons.add)),
label: Text(AppLocalizations.of(context)!.editItemShort),
icon: const Icon(Icons.edit)),
);
}
}

View file

@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.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/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
class NewItemPage extends StatefulWidget {
final String room;
final String server;
final int? item;
const NewItemPage(
{super.key, required this.room, required this.server, this.item});
@override
State<StatefulWidget> createState() => _NewItemPageState();
}
class _NewItemPageState extends State<NewItemPage> {
// input controllers
final _ctrInput = TextEditingController();
// data cache
List<RoomCategory> categories = [];
List<RoomProduct> products = [];
void fetchCategories() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// load cached categories
final cache = await RoomCategory.list(widget.server, widget.room);
if (mounted) {
setState(() {
categories = cache;
});
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList();
setState(() {
categories = resp;
});
});
}
void fetchProducts() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// load cached products first
final cache = await RoomProduct.list(widget.server, widget.room);
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList();
setState(() {
products = 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();
});
}
String _query = "";
void createItem(BuildContext ctx, String name, int? productID) {
final scaffMgr = ScaffoldMessenger.of(context);
final router = GoRouter.of(context);
final user = context.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
target: user.server,
credentials: user,
path: 'addItem',
body: {
'room': widget.room,
'server': widget.server,
'state': 0,
'title': name,
'description': '',
'listCatID': null,
'unit': 0,
'value': '',
'listProdID': productID
}),
onOK: (body) async {
final id = body["data"]["listItemID"];
// 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,
'id': widget.room,
'item': id.toString()
});
});
}
void createProduct(BuildContext ctx, String name, Function(int) cb) {
final scaffMgr = ScaffoldMessenger.of(context);
final user = context.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
target: user.server,
credentials: user,
path: 'addProduct',
body: {
'room': widget.room,
'server': widget.server,
'title': name,
'description': '',
'listCatID': null,
'defUnit': 0,
'defValue': '',
'ean': '',
'parent': null
}),
onOK: (body) async {
final id = body["data"]["listProdID"];
// 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);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text((widget.item == null)
? AppLocalizations.of(context)!.createItem
: AppLocalizations.of(context)!.editItem),
),
body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SearchBar(
controller: _ctrInput,
leading: const Icon(Icons.search),
hintText:
AppLocalizations.of(context)!.newItemQueryHint,
onChanged: (text) {
setState(() {
_query = text;
});
},
),
const Divider(),
// button bar
...((_query == "")
? ([
Text(AppLocalizations.of(context)!
.newItemQueryEmpty)
])
: ([
Wrap(
spacing: 20,
runSpacing: 10,
alignment: WrapAlignment.center,
crossAxisAlignment:
WrapCrossAlignment.center,
children: [
Text(AppLocalizations.of(context)!
.newItemQuickAccessPrefix),
// new item
FilledButton.icon(
onPressed: () {
// create new named item
// launch edit item screen once done
createItem(context, _query, null);
},
icon: const Icon(Icons.add),
label: Text(_query)),
// new product
FilledButton.icon(
onPressed: () {
// create new product with name,
// create new item with name
// and link to the created product
// launch edit item screen once done
createProduct(
context,
_query,
(p0) => createItem(
context, _query, p0));
},
icon: const Icon(Icons.add),
label: Text(AppLocalizations.of(
context)!
.newItemQuickProduct(_query))),
])
])),
const Divider(),
// link products search
...((products
// show all products if query is empty
// when query isn't empty show products
// that contain the query in the title
// or description
.where((element) =>
(_query == "") ||
element.name.contains(_query) ||
element.description.contains(_query))
.map((e) => ListTile(
title: Text(e.name),
subtitle: Text(e.description),
trailing: CategoryChip(
server: widget.server,
room: widget.room,
category: categories
.where((element) =>
element.id == e.category)
.firstOrNull ??
RoomCategory.other(widget.server,
widget.room, context),
),
onTap: () {
// create new item and link it to the product
// launch edit item screen once done
createItem(
context,
// use productname as item name
e.name,
e.id);
},
))))
],
))))),
);
}
}

View file

@ -105,7 +105,7 @@ class _JoinRoomPageState extends State {
icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context)!.search,
onPressed: () {
// show searchbar
// TODO: show searchbar
// NOTE: location currently unknown
},
),
@ -170,7 +170,8 @@ class _JoinRoomPageState extends State {
return BottomSheet(
onClosing: () {},
builder: (ctx) {
return Column(
return SingleChildScrollView(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisAlignment:
@ -282,7 +283,7 @@ class _JoinRoomPageState extends State {
))
])
],
);
));
},
);
});

View file

@ -33,6 +33,10 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
return SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(children: [
// room meta display
...(widget.room != null)
@ -65,7 +69,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
showSelectedIcon: true,
multiSelectionEnabled: false,
emptySelectionAllowed: false,
segments: RoomVisibility.list().map((vis) {
segments:
RoomVisibility.list().map((vis) {
return ButtonSegment<int>(
value: vis.type,
label: Text(vis.text(context)),
@ -76,10 +81,14 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
// only show confirm dialog when user
// is admin, owner or has CHANGE_ADMIN permission
if (widget.info == null ||
(!(widget.info?.isAdmin ?? false) &&
!(widget.info?.isOwner ?? false) &&
((widget.info?.permissions)! &
RoomPermission.ota ==
(!(widget.info?.isAdmin ??
false) &&
!(widget.info?.isOwner ??
false) &&
((widget.info
?.permissions)! &
RoomPermission
.ota ==
0))) {
// action not permitted
// NOTE: no error dialog should be shown
@ -87,46 +96,65 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
return;
}
final vis = RoomVisibility(vset.first);
final vis =
RoomVisibility(vset.first);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context)!
title: Text(AppLocalizations
.of(context)!
.changeRoomVisibilityTitle),
content: Text(
AppLocalizations.of(context)!
content: Text(AppLocalizations
.of(context)!
.changeRoomVisibilitySubtitle(
vis.text(context))),
vis.text(
context))),
actions: [
TextButton(
onPressed: () {
context.pop();
},
child: Text(
AppLocalizations.of(context)!
AppLocalizations.of(
context)!
.cancel),
),
FilledButton(
onPressed: () async {
final scaffMgr =
ScaffoldMessenger.of(context);
final nav = Navigator.of(context);
final user = context.read<User>();
ScaffoldMessenger
.of(context);
final nav =
Navigator.of(
context);
final user = context
.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: 'setVisibility',
target: user.server,
doNetworkRequest(
scaffMgr,
req: () =>
postWithCreadentials(
path:
'setVisibility',
target: user
.server,
body: {
'room': widget.room?.id,
'room': widget
.room
?.id,
'server': (widget
.room?.serverTag)!,
'visibility': vset.first
.room
?.serverTag)!,
'visibility':
vset.first
},
credentials: user),
credentials:
user),
onOK: (_) {
Room r = widget.room!;
r.visibility = vis;
Room r = widget
.room!;
r.visibility =
vis;
r.toDisk();
},
after: () {
@ -134,13 +162,18 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
});
},
child: Text(
AppLocalizations.of(context)!.ok),
AppLocalizations.of(
context)!
.ok),
)
],
));
}),
selected: {(widget.room?.visibility?.type)!},
selectedIcon: Icon((widget.room?.visibility?.icon)!),
selected: {
(widget.room?.visibility?.type)!
},
selectedIcon: Icon(
(widget.room?.visibility?.icon)!),
)),
],
),
@ -161,10 +194,13 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
0)))
? [
ListTile(
trailing: const Icon(Icons.chevron_right),
trailing:
const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!.editRoomMetadata),
subtitle: Text(AppLocalizations.of(context)!
AppLocalizations.of(context)!
.editRoomMetadata),
subtitle: Text(
AppLocalizations.of(context)!
.editRoomMetadataSubtitle),
onTap: () {
// show edit room screen
@ -179,9 +215,10 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
// open members view
ListTile(
trailing: const Icon(Icons.chevron_right),
title: Text(AppLocalizations.of(context)!.showRoomMembers),
subtitle:
Text(AppLocalizations.of(context)!.showRoomMembersSubtitle),
title: Text(AppLocalizations.of(context)!
.showRoomMembers),
subtitle: Text(AppLocalizations.of(context)!
.showRoomMembersSubtitle),
onTap: () {
// open member view screen
context.goNamed('room-members', params: {
@ -199,15 +236,20 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
0)))
? [
ListTile(
trailing: const Icon(Icons.chevron_right),
trailing:
const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!.editRoomPermissions),
subtitle: Text(AppLocalizations.of(context)!
AppLocalizations.of(context)!
.editRoomPermissions),
subtitle: Text(
AppLocalizations.of(context)!
.editRoomPermissionsSubtitle),
onTap: () {
// show checkbox screen
context.goNamed('room-permissions', params: {
'server': (widget.room?.serverTag)!,
context.goNamed('room-permissions',
params: {
'server':
(widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
},
@ -217,14 +259,18 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
...(widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & RoomPermission.ota !=
((widget.info?.permissions)! &
RoomPermission.ota !=
0)))
? [
ListTile(
trailing: const Icon(Icons.chevron_right),
title:
Text(AppLocalizations.of(context)!.manageRoomOTA),
subtitle: Text(AppLocalizations.of(context)!
trailing:
const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!
.manageRoomOTA),
subtitle: Text(
AppLocalizations.of(context)!
.manageRoomOTASubtitle),
onTap: () {
// show manage ota screen
@ -235,14 +281,18 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
},
),
ListTile(
trailing: const Icon(Icons.chevron_right),
trailing:
const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!.manageRoomInvites),
subtitle: Text(AppLocalizations.of(context)!
AppLocalizations.of(context)!
.manageRoomInvites),
subtitle: Text(
AppLocalizations.of(context)!
.manageRoomInvitesSubtitle),
onTap: () {
// show manage ota screen
context.goNamed('room-invite', params: {
context
.goNamed('room-invite', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
@ -259,20 +309,30 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
padding: const EdgeInsets.all(8),
child: FilledButton.tonal(
child: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)!.deleteRoom
: AppLocalizations.of(context)!.leaveRoom),
? AppLocalizations.of(context)!
.deleteRoom
: AppLocalizations.of(context)!
.leaveRoom),
onPressed: () {
// show confirm dialog
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)!.deleteRoom
: AppLocalizations.of(context)!.leaveRoom),
content: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)!
title: Text(
((widget.info?.isOwner)!)
? AppLocalizations.of(
context)!
.deleteRoom
: AppLocalizations.of(
context)!
.leaveRoom),
content: Text(
((widget.info?.isOwner)!)
? AppLocalizations.of(
context)!
.deleteRoomConfirm
: AppLocalizations.of(context)!
: AppLocalizations.of(
context)!
.leaveRoomConfirm),
actions: [
TextButton(
@ -281,47 +341,67 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
Navigator.of(ctx).pop();
},
child: Text(
AppLocalizations.of(context)!.cancel),
AppLocalizations.of(
context)!
.cancel),
),
FilledButton(
onPressed: () async {
// send request
final scaffMgr =
ScaffoldMessenger.of(ctx);
final nav = Navigator.of(ctx);
final router = GoRouter.of(context);
final user = context.read<User>();
ScaffoldMessenger.of(
ctx);
final nav =
Navigator.of(ctx);
final router =
GoRouter.of(context);
final user =
context.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: ((widget.info?.isOwner)!)
req: () =>
postWithCreadentials(
path: ((widget
.info
?.isOwner)!)
? 'deleteRoom'
: 'leaveRoom',
target: user.server,
target: user
.server,
body: {
'room': widget.room?.id,
'server':
(widget.room?.serverTag)!,
'room': widget
.room
?.id,
'server': (widget
.room
?.serverTag)!,
},
credentials: user),
credentials:
user),
onOK: (_) async {
// try delete room from disk
try {
await widget.room?.removeDisk();
await widget.room
?.removeDisk();
} catch (_) {}
// go back home
router.pushReplacementNamed('home');
router
.pushReplacementNamed(
'home');
},
after: () {
// close popup
nav.pop();
});
},
child: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)!
child: Text(((widget
.info?.isOwner)!)
? AppLocalizations.of(
context)!
.deleteRoomShort
: AppLocalizations.of(context)!
: AppLocalizations.of(
context)!
.leaveRoomShort),
)
],
@ -330,6 +410,6 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
))
]
: [],
])));
])))));
}
}

View file

@ -25,6 +25,18 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
void 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((_) {
fetchCategories();
});
@ -32,10 +44,18 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
void fetchCategories() async {
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(
credentials: user,
target: user.server,
@ -43,8 +63,12 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (json) {
final resp = json['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.map<RoomCategory>((raw) => RoomCategory.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList();
for (RoomCategory ce in resp) {
ce.toDisk();
}
if (mounted) {
setState(() {
@ -61,7 +85,12 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
return Scaffold(
body: ReorderableListView.builder(
body: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final item = list[index];
@ -75,7 +104,8 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
RoomPermission.editRoomContent !=
0))
? ReorderableDragStartListener(
index: index, child: const Icon(Icons.drag_handle))
index: index,
child: const Icon(Icons.drag_handle))
: null,
title: Text(item.name),
onTap: () {
@ -99,20 +129,24 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
Icon(Icons.square_rounded,
size: 48.0, color: item.color),
Text(item.name, style: textTheme.titleLarge)
Text(item.name,
style: textTheme.titleLarge)
],
)),
// edit category
ListTile(
leading: const Icon(Icons.edit),
title: Text(AppLocalizations.of(context)!.editCategory),
subtitle:
Text(AppLocalizations.of(context)!.editCategoryLong),
title: Text(AppLocalizations.of(context)!
.editCategory),
subtitle: Text(AppLocalizations.of(context)!
.editCategoryLong),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// close the modal bottom sheet
@ -131,9 +165,10 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
// delete category
ListTile(
leading: const Icon(Icons.delete),
title: Text(AppLocalizations.of(context)!.deleteCategory),
subtitle: Text(
AppLocalizations.of(context)!.deleteCategoryLong),
title: Text(AppLocalizations.of(context)!
.deleteCategory),
subtitle: Text(AppLocalizations.of(context)!
.deleteCategoryLong),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// show popup
@ -141,10 +176,13 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
context: context,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete),
title: Text(AppLocalizations.of(context)!
title: Text(
AppLocalizations.of(context)!
.deleteCategory),
content: Text(AppLocalizations.of(context)!
.deleteCategoryConfirm(item.name)),
content: Text(
AppLocalizations.of(context)!
.deleteCategoryConfirm(
item.name)),
actions: [
TextButton(
onPressed: () {
@ -152,32 +190,46 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
Navigator.of(ctx).pop();
},
child: Text(
AppLocalizations.of(context)!.cancel),
AppLocalizations.of(
context)!
.cancel),
),
FilledButton(
onPressed: () async {
// send request
final scaffMgr =
ScaffoldMessenger.of(ctx);
ScaffoldMessenger.of(
ctx);
// popup context
final navInner = Navigator.of(ctx);
final navInner =
Navigator.of(ctx);
// bottomsheet context
final nav = Navigator.of(context);
final user = context.read<User>();
final nav =
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: 'deleteCategory',
target: user.server,
req: () =>
postWithCreadentials(
path:
'deleteCategory',
target:
user.server,
body: {
'room': widget.room?.id,
'server':
widget.room?.serverTag,
'listCatID': item.id
'room': widget
.room?.id,
'server': widget
.room
?.serverTag,
'listCatID':
item.id
},
credentials: user),
credentials:
user),
onOK: (_) async {
// TODO: remove cached category
// remove cached category
item.removeDisk();
fetchCategories();
},
after: () {
@ -187,7 +239,9 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!
child: Text(
AppLocalizations.of(
context)!
.deleteCategory),
)
],
@ -205,7 +259,8 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
onReorder: (int oldIndex, int newIndex) {
if (!((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & RoomPermission.editRoomContent !=
((widget.info?.permissions)! &
RoomPermission.editRoomContent !=
0))) {
// user is not allowed to edit or delete categories
return;
@ -228,11 +283,12 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
body: {
'room': widget.room?.id,
'server': widget.room?.serverTag,
'listCatIDs': list.map((item) => item.id).toList()
'listCatIDs':
list.map((item) => item.id).toList()
}));
});
},
),
)))),
floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||

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/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/components/value_unit_input.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
@ -30,12 +28,37 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
Map<int?, RoomCategory> categories = {};
List<RoomProduct> products = [];
void fetchItems() {
void fetchItems() async {
final user = context.read<User>();
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<RoomItem> l = [];
final List<RoomItem> 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,
@ -43,7 +66,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomItem>((raw) => RoomItem.fromJSON(raw))
.map<RoomItem>((raw) => RoomItem.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList();
final List<RoomItem> l = [];
@ -55,10 +79,10 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
} else {
c.add(item);
}
// cache items
await item.toDisk();
}
// TODO: cache items
if (mounted) {
setState(() {
list = l;
@ -108,12 +132,31 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
}
}
void fetchCategories() {
void fetchCategories() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached categories first
// load cached categories
final resp = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
Map<int, int> map = {};
Map<int?, RoomCategory> cat = {};
for (int i = 0; i < resp.length; i++) {
map[resp[i].id ?? 0] = i;
cat[resp[i].id ?? 0] = resp[i];
}
doNetworkRequest(ScaffoldMessenger.of(context),
if (mounted) {
setState(() {
weights = map;
categories = cat;
sortAll();
});
}
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
@ -121,7 +164,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.map<RoomCategory>((raw) => RoomCategory.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList();
Map<int, int> map = {};
@ -141,12 +185,20 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
});
}
void fetchProducts() {
void fetchProducts() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached products first
// load cached products first
final cache = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(ScaffoldMessenger.of(context),
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
@ -154,7 +206,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList();
if (mounted) {
@ -169,6 +222,68 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
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<RoomItem> l = [];
final List<RoomItem> 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 {
try {
final updated = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
setState(() {
products = updated;
});
} catch (_) {}
});
// wait for background room category changes
RoomCategory.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final resp = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
Map<int, int> map = {};
Map<int?, RoomCategory> 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();
});
}
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchItems();
fetchCategories();
@ -194,7 +309,12 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(children: [
body: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView(children: [
LabeledDivider(AppLocalizations.of(context)!.shoppingList),
ListView.builder(
shrinkWrap: true,
@ -202,10 +322,13 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
final cat =
categories[item.category] ?? RoomCategory.other(context);
final cat = categories[item.category] ??
RoomCategory.other(widget.room?.serverTag ?? "",
widget.room?.id ?? "", context);
return ShoppingListItem(
name: item.name,
server: widget.room!.serverTag,
room: widget.room!.id,
description: item.description,
category: cat,
key: Key(item.id.toString()),
@ -253,10 +376,13 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
final item = cart[index];
final cat =
categories[item.category] ?? RoomCategory.other(context);
final cat = categories[item.category] ??
RoomCategory.other(widget.room!.serverTag,
widget.room!.id, context);
return ShoppingListItem(
server: widget.room!.serverTag,
room: widget.room!.id,
name: item.name,
description: item.description,
category: cat,
@ -297,7 +423,7 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
});
},
)
]),
])))),
floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
@ -329,12 +455,16 @@ class ShoppingListItem extends StatelessWidget {
final Key _key;
final Function()? onDismiss;
final Function()? onTap;
final String server;
final String room;
const ShoppingListItem(
{required this.name,
required this.category,
required this.inCart,
required this.description,
required this.server,
required this.room,
required key,
this.onDismiss,
this.onTap})
@ -367,6 +497,8 @@ class ShoppingListItem extends StatelessWidget {
title: Text(name),
subtitle: Text(description),
trailing: CategoryChip(
server: server,
room: room,
category: category,
),
onTap: () {
@ -410,6 +542,8 @@ class ShoppingListItemInfo extends StatelessWidget {
Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium),
CategoryChip(
server: server,
room: room,
category: category,
),
Text(Unit.fromId(item.unit).display(context, item.value))
@ -445,11 +579,14 @@ class ShoppingListItemInfo extends StatelessWidget {
subtitle: Text(AppLocalizations.of(context)!.editItemLong),
trailing: const Icon(Icons.chevron_right),
onTap: () {
context.pushNamed('edit-product', params: {
context.pushNamed('edit-item', params: {
'server': server,
'id': room,
'item': item.id.toString()
});
final navInner = Navigator.of(context);
navInner.pop();
},
),
ListTile(
@ -459,16 +596,70 @@ class ShoppingListItemInfo extends StatelessWidget {
AppLocalizations.of(context)!.deleteItemSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: show confirm dialog
// show popup
showDialog(
context: context,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete),
title: Text(
AppLocalizations.of(context)!.deleteItem),
content: Text(AppLocalizations.of(context)!
.deleteItemConfirm(item.name)),
actions: [
TextButton(
onPressed: () {
// close popup
Navigator.of(ctx).pop();
},
child: Text(
AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed: () async {
// send request
final scaffMgr =
ScaffoldMessenger.of(ctx);
// popup context
final navInner = Navigator.of(ctx);
// bottomsheet context
final nav = Navigator.of(context);
final user = context.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: 'deleteItem',
target: user.server,
body: {
'room': room,
'server': server,
'listItemID': item.id
},
credentials: user),
onOK: (_) async {
// remove cached item
await item.removeDisk();
},
after: () {
// close popup
navInner.pop();
// close modal bottom sheet
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!
.deleteItem),
)
],
));
}),
]
: [],
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
@ -484,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();
});
})
],
),

View file

@ -21,10 +21,20 @@ class RoomProductsPage extends StatefulWidget {
class _RoomProductsPageState extends State<RoomProductsPage> {
List<RoomProduct> products = [];
void fetchProducts() {
void fetchProducts() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
doNetworkRequest(ScaffoldMessenger.of(context),
// load cached products first
final cache = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
@ -32,10 +42,13 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList();
// TODO: cache products
for (RoomProduct prod in resp) {
prod.toDisk();
}
if (mounted) {
setState(() {
@ -49,13 +62,30 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
void initState() {
super.initState();
// wait for background room product changes
RoomProduct.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final updated = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
setState(() {
products = updated;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) => fetchProducts());
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
body: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final item = products[index];
@ -80,7 +110,7 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
},
);
},
),
)))),
floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||

View file

@ -36,12 +36,19 @@ class _EditProductPageState extends State<EditProductPage> {
List<RoomCategory> categories = [];
List<RoomProduct> products = [];
void fetchCategories() {
void fetchCategories() async {
final user = context.read<User>();
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,
@ -49,7 +56,8 @@ class _EditProductPageState extends State<EditProductPage> {
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList();
setState(() {
@ -58,12 +66,19 @@ class _EditProductPageState extends State<EditProductPage> {
});
}
void fetchProducts() {
void fetchProducts() async {
final user = context.read<User>();
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,
@ -71,7 +86,8 @@ class _EditProductPageState extends State<EditProductPage> {
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList();
if (widget.product != null) {
@ -102,6 +118,16 @@ class _EditProductPageState extends State<EditProductPage> {
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();
@ -192,6 +218,8 @@ class _EditProductPageState extends State<EditProductPage> {
},
),
CategoryPicker(
server: widget.server,
room: widget.room,
label: AppLocalizations.of(context)!
.selectCategoryLabel,
hint: AppLocalizations.of(context)!
@ -252,7 +280,22 @@ class _EditProductPageState extends State<EditProductPage> {
'ean': _ctrEAN.text,
'parent': _ctrParent
}),
onOK: (_) async {
onOK: (body) async {
// cache product
final id = body["data"]["listProdID"];
final prod = RoomProduct(
id: id,
name: _ctrName.text,
server: widget.server,
room: widget.room,
description: _ctrDescription.text,
category: _ctrCategory,
defaultUnit: _ctrUnit,
defaultValue: _ctrValue,
ean: _ctrEAN.text,
parent: _ctrParent);
await prod.toDisk();
nav.pop();
});
} else {
@ -274,6 +317,20 @@ class _EditProductPageState extends State<EditProductPage> {
'parent': _ctrParent
}),
onOK: (_) async {
// cache product
final prod = RoomProduct(
id: widget.product!,
name: _ctrName.text,
server: widget.server,
room: widget.room,
description: _ctrDescription.text,
category: _ctrCategory,
defaultUnit: _ctrUnit,
defaultValue: _ctrValue,
ean: _ctrEAN.text,
parent: _ctrParent);
await prod.toDisk();
nav.pop();
});
}

View file

@ -53,12 +53,24 @@ class _ViewProductPageState extends State<ViewProductPage> {
);
}
void fetchCategories() {
void fetchCategories() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached categories first
// load cached categories
final cache = await RoomCategory.list(widget.server, widget.room);
if (mounted) {
Map<int?, RoomCategory> map = {};
doNetworkRequest(ScaffoldMessenger.of(context),
for (RoomCategory cat in cache) {
map[cat.id] = cat;
}
setState(() {
categories = map;
});
}
doNetworkRequest(scaffmgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
@ -66,7 +78,8 @@ class _ViewProductPageState extends State<ViewProductPage> {
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList();
Map<int?, RoomCategory> map = {};
@ -80,12 +93,19 @@ class _ViewProductPageState extends State<ViewProductPage> {
});
}
void fetchProducts() {
void fetchProducts() async {
final user = context.read<User>();
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,
@ -93,7 +113,8 @@ class _ViewProductPageState extends State<ViewProductPage> {
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList();
for (RoomProduct prod in resp) {
@ -115,6 +136,39 @@ class _ViewProductPageState extends State<ViewProductPage> {
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);
for (RoomProduct prod in updated) {
// load product info
// for current product
if (prod.id == widget.product) {
setState(() {
product = prod;
});
}
}
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);
Map<int?, RoomCategory> map = {};
for (RoomCategory cat in updated) {
map[cat.id] = cat;
}
setState(() {
categories = map;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories();
fetchProducts();
@ -131,6 +185,11 @@ class _ViewProductPageState extends State<ViewProductPage> {
title: Text(product?.name ?? ''),
),
body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(children: [
// display product into
Center(
@ -140,14 +199,20 @@ class _ViewProductPageState extends State<ViewProductPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(product?.name ?? '', style: textTheme.headlineLarge),
Text(product?.name ?? '',
style: textTheme.headlineLarge),
Text(product?.description ?? '',
style: textTheme.titleMedium),
Text(product?.ean ?? ''),
CategoryChip(category: categories[product?.category]),
CategoryChip(
server: widget.server,
room: widget.room,
category:
categories[product?.category]),
Text(product != null
? Unit.fromId(product!.defaultUnit)
.display(context, product!.defaultValue)
.display(
context, product!.defaultValue)
: '')
],
))),
@ -157,12 +222,15 @@ class _ViewProductPageState extends State<ViewProductPage> {
...(info != null &&
(info!.isAdmin ||
info!.isOwner ||
(info!.permissions & RoomPermission.editRoomContent != 0)))
(info!.permissions &
RoomPermission.editRoomContent !=
0)))
? [
ListTile(
title: Text(AppLocalizations.of(context)!.editProductTitle),
subtitle:
Text(AppLocalizations.of(context)!.editProductSubtitle),
title: Text(AppLocalizations.of(context)!
.editProductTitle),
subtitle: Text(AppLocalizations.of(context)!
.editProductSubtitle),
onTap: () {
context.pushNamed('edit-product', params: {
'server': widget.server,
@ -178,10 +246,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
...(product?.parent != null)
? [
ListTile(
title: Text(
AppLocalizations.of(context)!.viewParentProductTitle),
subtitle: Text(
AppLocalizations.of(context)!.viewParentProductSubtitle),
title: Text(AppLocalizations.of(context)!
.viewParentProductTitle),
subtitle: Text(AppLocalizations.of(context)!
.viewParentProductSubtitle),
onTap: () {
context.pushNamed('view-product', params: {
'server': widget.server,
@ -195,9 +263,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
: [],
// show/manage children
ListTile(
title: Text(AppLocalizations.of(context)!.viewProductChildrenTitle),
subtitle:
Text(AppLocalizations.of(context)!.viewProductChildrenSubtitle),
title: Text(AppLocalizations.of(context)!
.viewProductChildrenTitle),
subtitle: Text(AppLocalizations.of(context)!
.viewProductChildrenSubtitle),
onTap: () {
context.pushNamed('view-product-children', params: {
'server': widget.server,
@ -207,7 +276,101 @@ class _ViewProductPageState extends State<ViewProductPage> {
},
trailing: const Icon(Icons.chevron_right),
),
])),
...(info != null &&
((info?.isAdmin ?? false) ||
(info?.isOwner ?? false) ||
((info?.permissions)! &
RoomPermission.editRoomContent !=
0)))
? [
// delete product
ListTile(
title: Text(AppLocalizations.of(context)!
.deleteProductTitle),
subtitle: Text(AppLocalizations.of(context)!
.deleteProductSubtitle),
onTap: () {
// show popup
showDialog(
context: context,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete),
title: Text(
AppLocalizations.of(context)!
.deleteProduct),
content: Text(
AppLocalizations.of(context)!
.deleteProductConfirm(
product?.name ?? "")),
actions: [
TextButton(
onPressed: () {
// close popup
Navigator.of(ctx).pop();
},
child: Text(
AppLocalizations.of(
context)!
.cancel),
),
FilledButton(
onPressed: () async {
// send request
final scaffMgr =
ScaffoldMessenger.of(
ctx);
// popup context
final navInner =
Navigator.of(ctx);
// bottomsheet context
final nav =
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(scaffMgr,
req: () =>
postWithCreadentials(
path:
'deleteProduct',
target:
user.server,
body: {
'room': widget
.room,
'server': widget
.server,
'listProdID':
product?.id ??
""
},
credentials:
user),
onOK: (_) async {
// remove cached product
await product!
.removeDisk();
},
after: () {
// close popup
navInner.pop();
// close modal bottom sheet
nav.pop();
});
},
child: Text(
AppLocalizations.of(
context)!
.deleteProduct),
)
],
));
},
trailing: const Icon(Icons.chevron_right),
),
]
: []
]))))),
);
}
}

View file

@ -71,6 +71,6 @@ Future<void> doNetworkRequest(ScaffoldMessengerState? sm,
}
if (after != null) {
after();
await after();
}
}

View file

@ -5,26 +5,26 @@ packages:
dependency: transitive
description:
name: archive
sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
url: "https://pub.dev"
source: hosted
version: "3.3.6"
version: "3.4.9"
args:
dependency: transitive
description:
name: args
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.2"
async:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
@ -37,18 +37,18 @@ packages:
dependency: transitive
description:
name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.0.3"
cli_util:
dependency: transitive
description:
@ -69,10 +69,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.0"
convert:
dependency: transitive
description:
@ -85,10 +85,10 @@ packages:
dependency: "direct main"
description:
name: crypto
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
fake_async:
dependency: transitive
description:
@ -101,18 +101,18 @@ packages:
dependency: transitive
description:
name: ffi
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.1.0"
file:
dependency: transitive
description:
name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "7.0.0"
flutter:
dependency: "direct main"
description: flutter
@ -130,10 +130,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -143,10 +143,10 @@ packages:
dependency: "direct main"
description:
name: flutter_svg
sha256: "12006889e2987c549c4c1ec1a5ba4ec4b24d34d2469ee5f9476c926dcecff266"
sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.0.9"
flutter_test:
dependency: "direct dev"
description: flutter
@ -161,18 +161,18 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "432409518740645ce7f28802171b78783197d01149fad44f9b8ae55f40277139"
sha256: bd7e671d26fd39c78cba82070fa34ef1f830b0e7ed1aeebccabc6561302a7ee5
url: "https://pub.dev"
source: hosted
version: "6.5.0"
version: "6.5.9"
http:
dependency: "direct main"
description:
name: http
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
url: "https://pub.dev"
source: hosted
version: "0.13.5"
version: "0.13.6"
http_parser:
dependency: transitive
description:
@ -185,42 +185,42 @@ packages:
dependency: transitive
description:
name: image
sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227"
sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
url: "https://pub.dev"
source: hosted
version: "4.0.15"
version: "4.1.3"
intl:
dependency: "direct main"
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
version: "0.18.1"
js:
dependency: transitive
description:
name: js
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.7"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
url: "https://pub.dev"
source: hosted
version: "4.8.0"
version: "4.8.1"
lints:
dependency: transitive
description:
name: lints
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.1.1"
localstore:
dependency: "direct main"
description:
@ -233,34 +233,34 @@ packages:
dependency: transitive
description:
name: logging
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.2.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.13"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.5.0"
meta:
dependency: transitive
description:
name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
version: "1.8.0"
version: "1.10.0"
nested:
dependency: transitive
description:
@ -273,10 +273,10 @@ packages:
dependency: transitive
description:
name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.2"
version: "1.8.3"
path_parsing:
dependency: transitive
description:
@ -289,154 +289,146 @@ packages:
dependency: transitive
description:
name: path_provider
sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9"
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev"
source: hosted
version: "2.0.13"
version: "2.1.1"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7"
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
url: "https://pub.dev"
source: hosted
version: "2.0.24"
version: "2.2.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "12eee51abdf4d34c590f043f45073adbb45514a108bd9db4491547a2fd891059"
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.1.10"
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
url: "https://pub.dev"
source: hosted
version: "2.0.6"
version: "2.1.1"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "5.1.0"
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.3"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.7"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: ae73e842cdd27a3467a71d70cefd9b198538aab4fc7dde1d0e8c78c96225abf0
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
url: "https://pub.dev"
source: hosted
version: "3.7.1"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
version: "3.7.3"
provider:
dependency: "direct main"
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
url: "https://pub.dev"
source: hosted
version: "6.0.5"
version: "6.1.1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394"
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
url: "https://pub.dev"
source: hosted
version: "2.0.20"
version: "2.2.2"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
url: "https://pub.dev"
source: hosted
version: "2.0.17"
version: "2.2.1"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310"
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.3.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707"
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc"
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.3.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8"
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
version: "2.0.6"
version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436"
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.3.2"
sky_engine:
dependency: transitive
description: flutter
@ -446,26 +438,26 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
string_scanner:
dependency: transitive
description:
@ -486,42 +478,42 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.dev"
source: hosted
version: "0.4.16"
version: "0.6.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "4cf8e60dbe4d3a693d37dff11255a172594c0793da542183cbfe7fe978ae4aaa"
sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
version: "1.1.9+1"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "278ad5f816f58b1967396d1f78ced470e3e58c9fe4b27010102c0a595c764468"
sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
version: "1.1.9+1"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "0bf61ad56e6fd6688a2865d3ceaea396bc6a0a90ea0d7ad5049b1b76c09d6163"
sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26
url: "https://pub.dev"
source: hosted
version: "1.1.4"
version: "1.1.9+1"
vector_math:
dependency: transitive
description:
@ -530,38 +522,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev"
source: hosted
version: "0.3.0"
win32:
dependency: transitive
description:
name: win32
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "5.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.3"
xml:
dependency: transitive
description:
name: xml
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.2.2"
version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
sdks:
dart: ">=2.19.3 <3.0.0"
flutter: ">=3.7.0-0"
dart: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"

View file

@ -1,3 +1,4 @@
---
name: outbag_app
description: Official Outbag App
# The following line prevents the package from being accidentally published to