Compare commits

...

10 commits

Author SHA1 Message Date
9332f72cca
build action, version update
Some checks failed
Tag Action / build (push) Failing after 1m20s
2024-02-25 21:27:41 +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
29 changed files with 2280 additions and 997 deletions

View file

@ -0,0 +1,18 @@
name: Tag Action
on:
push:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: apt-get update && apt-get install jq -y
- uses: subosito/flutter-action@v2
with:
channel: 'beta'
- run: flutter --version
- run: flutter build apk

View file

@ -1,11 +1,11 @@
# This file tracks properties of this Flutter project. # This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc. # Used by Flutter tool to assess capabilities and perform upgrades etc.
# #
# This file should be version controlled. # This file should be version controlled and should not be manually edited.
version: version:
revision: 2ad6cd72c040113b47ee9055e722606a490ef0da revision: "1751123cde4ffad08ae27bdee4f8ddebd033fe76"
channel: unknown channel: "beta"
project_type: app project_type: app
@ -13,26 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da create_revision: 1751123cde4ffad08ae27bdee4f8ddebd033fe76
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da base_revision: 1751123cde4ffad08ae27bdee4f8ddebd033fe76
- platform: android - platform: android
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da create_revision: 1751123cde4ffad08ae27bdee4f8ddebd033fe76
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da base_revision: 1751123cde4ffad08ae27bdee4f8ddebd033fe76
- platform: ios
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: linux
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: macos
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: web
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
- platform: windows
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
# User provided section # User provided section

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. 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/) [![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
```

View file

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@ -21,12 +22,9 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion namespace "com.example.app"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
@ -44,11 +42,11 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.outbag_app" applicationId "com.example.app"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion minSdk flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdk flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@ -66,6 +64,4 @@ flutter {
source '../..' source '../..'
} }
dependencies { dependencies {}
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View file

@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects { allprojects {
repositories { repositories {
google() google()
@ -26,6 +13,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View file

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

View file

@ -1,11 +1,25 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties() def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk") def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"

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 arb-dir: lib/l10n
template-arb-file: app_en.arb template-arb-file: app_en.arb
output-localization-file: app_localizations.dart output-localization-file: app_localizations.dart

View file

@ -363,20 +363,30 @@ class RoomInfo {
class RoomCategory { class RoomCategory {
final int? id; final int? id;
final String name; String name;
final ColorSwatch<int> color; ColorSwatch<int> color;
final String room;
final String server;
const RoomCategory( RoomCategory(
{required this.id, required this.name, required this.color}); {required this.id,
required this.name,
required this.color,
required this.server,
required this.room});
factory RoomCategory.fromJSON(dynamic json) { factory RoomCategory.fromJSON(String server, String room, dynamic json) {
return RoomCategory( return RoomCategory(
server: server,
room: room,
id: json['id'], id: json['id'],
name: json['title'], name: json['title'],
color: colorFromString(json['color'])); color: colorFromString(json['color']));
} }
factory RoomCategory.other(BuildContext context) { factory RoomCategory.other(String server, String room, BuildContext context) {
return RoomCategory( return RoomCategory(
server: server,
room: room,
id: null, id: null,
name: AppLocalizations.of(context)!.categoryNameOther, name: AppLocalizations.of(context)!.categoryNameOther,
color: Colors.grey); color: Colors.grey);
@ -398,6 +408,69 @@ class RoomCategory {
"purple-acc", "purple-acc",
].map((txt) => colorFromString(txt)).toList(); ].map((txt) => colorFromString(txt)).toList();
} }
// get list of all categories in a given room
static Future<List<RoomCategory>> list(String server, String room) async {
final db = Localstore.instance;
final rooms = (await db.collection('categories:$room@$server').get()) ?? {};
List<RoomCategory> builder = [];
for (MapEntry entry in rooms.entries) {
try {
builder.add(RoomCategory.fromMap(entry.value));
} catch (e) {
// skip invalid entries
// NOTE: might want to autodelete them in the future
// although keeping them might be ok,
// in case we ever get a new dataset to fix the current state
}
}
return builder;
}
// listen to room change
static listen(
String server, String room, Function(Map<String, dynamic>) cb) async {
final db = Localstore.instance;
final stream = db.collection('categories:$room@$server').stream;
stream.listen(cb);
}
factory RoomCategory.fromMap(Map<String, dynamic> map) {
return RoomCategory(
server: map['server'],
room: map['room'],
id: map['id'],
name: map['name'],
color: colorFromString(map['color']));
}
Map<String, dynamic> toMap() {
return {
'server': server,
'room': room,
'id': id,
'name': name,
'color': colorIdFromColor(color)
};
}
Future<void> toDisk() async {
final db = Localstore.instance;
await db.collection('categories:$room@$server').doc('$id').set(toMap());
}
Future<void> removeDisk() async {
final db = Localstore.instance;
await db.collection('categories:$room@$server').doc('$id').delete();
}
static Future<RoomCategory> fromDisk(
{required int id, required String server, required String room}) async {
final db = Localstore.instance;
final raw =
await db.collection('categories:$room@$server').doc('$id').get();
return RoomCategory.fromMap(raw!);
}
} }
ColorSwatch<int> colorFromString(String text) { ColorSwatch<int> colorFromString(String text) {
@ -474,7 +547,7 @@ String colorIdFromColor(ColorSwatch<int> color) {
} }
class RoomProduct { class RoomProduct {
int id; final int id;
String name; String name;
String description; String description;
// category ID // category ID
@ -490,9 +563,14 @@ class RoomProduct {
// parent product ID // parent product ID
int? parent; int? parent;
final String server;
final String room;
RoomProduct( RoomProduct(
{required this.id, {required this.id,
required this.name, required this.name,
required this.server,
required this.room,
this.description = '', this.description = '',
this.category = -1, this.category = -1,
this.defaultUnit = 0, this.defaultUnit = 0,
@ -500,8 +578,10 @@ class RoomProduct {
this.ean, this.ean,
this.parent}); this.parent});
factory RoomProduct.fromJSON(dynamic json) { factory RoomProduct.fromJSON(String server, String room, dynamic json) {
return RoomProduct( return RoomProduct(
server: server,
room: room,
id: json['listProdID'], id: json['listProdID'],
name: json['title'], name: json['title'],
description: json['description'], description: json['description'],
@ -511,10 +591,82 @@ class RoomProduct {
ean: json['ean'], ean: json['ean'],
parent: json['parent']); 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 { class RoomItem {
int id; final int id;
int state; int state;
String name; String name;
String description; String description;
@ -527,9 +679,14 @@ class RoomItem {
// may link to a product // may link to a product
int? link; int? link;
final String server;
final String room;
RoomItem( RoomItem(
{required this.id, {required this.id,
required this.name, required this.name,
required this.server,
required this.room,
this.description = '', this.description = '',
this.state = 0, this.state = 0,
this.category = -1, this.category = -1,
@ -537,10 +694,12 @@ class RoomItem {
this.value = '', this.value = '',
this.link}); this.link});
factory RoomItem.fromJSON(dynamic json) { factory RoomItem.fromJSON(String server, String room, dynamic json) {
return RoomItem( return RoomItem(
id: json['listItemID'], id: json['listItemID'],
name: json['title'], name: json['title'],
server: server,
room: room,
description: json['description'], description: json['description'],
category: json['listCatID'], category: json['listCatID'],
state: json['state'], state: json['state'],
@ -553,10 +712,84 @@ class RoomItem {
return RoomItem( return RoomItem(
id: id, id: id,
name: name, name: name,
server: server,
room: room,
description: description, description: description,
category: category, category: category,
unit: unit, unit: unit,
value: value, value: value,
link: link); 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 { class CategoryChip extends StatelessWidget {
final RoomCategory? category; final RoomCategory? category;
final String server;
final String room;
const CategoryChip({super.key, this.category}); const CategoryChip(
{super.key, required this.server, required this.room, this.category});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActionChip( return ActionChip(
avatar: Icon(Icons.square_rounded, avatar: Icon(Icons.square_rounded,
color: category?.color ?? RoomCategory.other(context).color), color: category?.color ??
label: Text(category?.name ?? RoomCategory.other(context).name), RoomCategory.other(server, room, context).color),
label: Text(
category?.name ?? RoomCategory.other(server, room, context).name),
); );
} }
} }

View file

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

View file

@ -424,5 +424,17 @@
"moveItemToCartTitle": "Move to Cart", "moveItemToCartTitle": "Move to Cart",
"moveItemToCartSubtitle": "Mark item as in-cart, so others know, you bought it", "moveItemToCartSubtitle": "Mark item as in-cart, so others know, you bought it",
"removeItemFromCartTitle": "Remove from Cart", "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/backend/request.dart';
import 'package:outbag_app/screens/room/categories/edit.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/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/edit.dart';
import 'package:outbag_app/screens/room/products/view.dart'; import 'package:outbag_app/screens/room/products/view.dart';
@ -306,7 +307,7 @@ class _OutbagAppState extends State {
GoRoute( GoRoute(
name: 'new-item', name: 'new-item',
path: 'new-item', path: 'new-item',
builder: (context, state) => EditItemPage( builder: (context, state) => NewItemPage(
server: server:
state.params['server'] ?? '', state.params['server'] ?? '',
room: state.params['id'] ?? '', 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, itemCount: rooms.length,
itemBuilder: (ctx, i) { itemBuilder: (ctx, i) {
final room = rooms[i]; final room = rooms[i];
@ -134,27 +139,32 @@ class _HomePageState extends State<HomePage> {
child: InkWell( child: InkWell(
onTap: () { onTap: () {
// open room // open room
context.goNamed('room', context.goNamed('room', params: {
params: {'server': room.serverTag, 'id': room.id}); 'server': room.serverTag,
'id': room.id
});
}, },
onLongPress: () { onLongPress: () {
// open bottom sheet // open bottom sheet
// NOTE: feature yet to be confirmed // NOTE: feature yet to be confirmed
}, },
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), padding:
const EdgeInsets.fromLTRB(10, 5, 5, 10),
child: ListTile( child: ListTile(
title: Text(room.name), title: Text(room.name),
visualDensity: const VisualDensity(vertical: 3), visualDensity:
const VisualDensity(vertical: 3),
subtitle: Text(room.description), subtitle: Text(room.description),
leading: AspectRatio( leading: AspectRatio(
aspectRatio: 1 / 1, aspectRatio: 1 / 1,
child: SvgPicture.asset("${room.icon?.img}"), child:
SvgPicture.asset("${room.icon?.img}"),
), ),
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
)))); ))));
}, },
), )))),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
label: Text(AppLocalizations.of(context)!.addRoom), label: Text(AppLocalizations.of(context)!.addRoom),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),

View file

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

View file

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

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

View file

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

View file

@ -25,6 +25,18 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
void initState() { void initState() {
super.initState(); super.initState();
// wait for background room category changes
RoomCategory.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final updated = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
setState(() {
list = updated;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories(); fetchCategories();
}); });
@ -32,10 +44,18 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
void fetchCategories() async { void fetchCategories() async {
final user = context.read<User>(); final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// TODO: load cached rooms // load cached categories
final cache = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
setState(() {
list = cache;
});
}
doNetworkRequest(ScaffoldMessenger.of(context), doNetworkRequest(scaffmgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -43,8 +63,12 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (json) { onOK: (json) {
final resp = json['data'] final resp = json['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) => RoomCategory.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList(); .toList();
for (RoomCategory ce in resp) {
ce.toDisk();
}
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -61,7 +85,12 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
.apply(displayColor: Theme.of(context).colorScheme.onSurface); .apply(displayColor: Theme.of(context).colorScheme.onSurface);
return Scaffold( 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, buildDefaultDragHandles: false,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = list[index]; final item = list[index];
@ -75,7 +104,8 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
RoomPermission.editRoomContent != RoomPermission.editRoomContent !=
0)) 0))
? ReorderableDragStartListener( ? ReorderableDragStartListener(
index: index, child: const Icon(Icons.drag_handle)) index: index,
child: const Icon(Icons.drag_handle))
: null, : null,
title: Text(item.name), title: Text(item.name),
onTap: () { onTap: () {
@ -99,20 +129,24 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment:
crossAxisAlignment: CrossAxisAlignment.center, MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [ children: [
Icon(Icons.square_rounded, Icon(Icons.square_rounded,
size: 48.0, color: item.color), size: 48.0, color: item.color),
Text(item.name, style: textTheme.titleLarge) Text(item.name,
style: textTheme.titleLarge)
], ],
)), )),
// edit category // edit category
ListTile( ListTile(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
title: Text(AppLocalizations.of(context)!.editCategory), title: Text(AppLocalizations.of(context)!
subtitle: .editCategory),
Text(AppLocalizations.of(context)!.editCategoryLong), subtitle: Text(AppLocalizations.of(context)!
.editCategoryLong),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
// close the modal bottom sheet // close the modal bottom sheet
@ -131,9 +165,10 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
// delete category // delete category
ListTile( ListTile(
leading: const Icon(Icons.delete), leading: const Icon(Icons.delete),
title: Text(AppLocalizations.of(context)!.deleteCategory), title: Text(AppLocalizations.of(context)!
subtitle: Text( .deleteCategory),
AppLocalizations.of(context)!.deleteCategoryLong), subtitle: Text(AppLocalizations.of(context)!
.deleteCategoryLong),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
// show popup // show popup
@ -141,10 +176,13 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
title: Text(AppLocalizations.of(context)! title: Text(
AppLocalizations.of(context)!
.deleteCategory), .deleteCategory),
content: Text(AppLocalizations.of(context)! content: Text(
.deleteCategoryConfirm(item.name)), AppLocalizations.of(context)!
.deleteCategoryConfirm(
item.name)),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -152,32 +190,46 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
}, },
child: Text( child: Text(
AppLocalizations.of(context)!.cancel), AppLocalizations.of(
context)!
.cancel),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
// send request // send request
final scaffMgr = final scaffMgr =
ScaffoldMessenger.of(ctx); ScaffoldMessenger.of(
ctx);
// popup context // popup context
final navInner = Navigator.of(ctx); final navInner =
Navigator.of(ctx);
// bottomsheet context // bottomsheet context
final nav = Navigator.of(context); final nav =
final user = context.read<User>(); Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(scaffMgr, doNetworkRequest(scaffMgr,
req: () => postWithCreadentials( req: () =>
path: 'deleteCategory', postWithCreadentials(
target: user.server, path:
'deleteCategory',
target:
user.server,
body: { body: {
'room': widget.room?.id, 'room': widget
'server': .room?.id,
widget.room?.serverTag, 'server': widget
'listCatID': item.id .room
?.serverTag,
'listCatID':
item.id
}, },
credentials: user), credentials:
user),
onOK: (_) async { onOK: (_) async {
// TODO: remove cached category // remove cached category
item.removeDisk();
fetchCategories(); fetchCategories();
}, },
after: () { after: () {
@ -187,7 +239,9 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
nav.pop(); nav.pop();
}); });
}, },
child: Text(AppLocalizations.of(context)! child: Text(
AppLocalizations.of(
context)!
.deleteCategory), .deleteCategory),
) )
], ],
@ -205,7 +259,8 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
onReorder: (int oldIndex, int newIndex) { onReorder: (int oldIndex, int newIndex) {
if (!((widget.info?.isAdmin ?? false) || if (!((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & RoomPermission.editRoomContent != ((widget.info?.permissions)! &
RoomPermission.editRoomContent !=
0))) { 0))) {
// user is not allowed to edit or delete categories // user is not allowed to edit or delete categories
return; return;
@ -228,11 +283,12 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
body: { body: {
'room': widget.room?.id, 'room': widget.room?.id,
'server': widget.room?.serverTag, 'server': widget.room?.serverTag,
'listCatIDs': list.map((item) => item.id).toList() 'listCatIDs':
list.map((item) => item.id).toList()
})); }));
}); });
}, },
), )))),
floatingActionButton: (widget.info != null && floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) || ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? 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/room.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/components/category_chip.dart'; import 'package:outbag_app/components/category_chip.dart';
import 'package:outbag_app/components/category_picker.dart';
import 'package:outbag_app/components/labeled_divider.dart'; import 'package:outbag_app/components/labeled_divider.dart';
import 'package:outbag_app/components/product_picker.dart';
import 'package:outbag_app/components/value_unit_input.dart'; import 'package:outbag_app/components/value_unit_input.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -30,12 +28,37 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
Map<int?, RoomCategory> categories = {}; Map<int?, RoomCategory> categories = {};
List<RoomProduct> products = []; List<RoomProduct> products = [];
void fetchItems() { void fetchItems() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -43,7 +66,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomItem>((raw) => RoomItem.fromJSON(raw)) .map<RoomItem>((raw) => RoomItem.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList(); .toList();
final List<RoomItem> l = []; final List<RoomItem> l = [];
@ -55,10 +79,10 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
} else { } else {
c.add(item); c.add(item);
} }
// cache items
await item.toDisk();
} }
// TODO: cache items
if (mounted) { if (mounted) {
setState(() { setState(() {
list = l; list = l;
@ -108,12 +132,31 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
} }
} }
void fetchCategories() { void fetchCategories() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -121,7 +164,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) => RoomCategory.fromJSON(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
.toList(); .toList();
Map<int, int> map = {}; Map<int, int> map = {};
@ -141,12 +185,20 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
}); });
} }
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -154,7 +206,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList(); .toList();
if (mounted) { if (mounted) {
@ -169,6 +222,68 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
void initState() { void initState() {
super.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((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
fetchItems(); fetchItems();
fetchCategories(); fetchCategories();
@ -194,7 +309,12 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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), LabeledDivider(AppLocalizations.of(context)!.shoppingList),
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
@ -202,10 +322,13 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
itemCount: list.length, itemCount: list.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = list[index]; final item = list[index];
final cat = final cat = categories[item.category] ??
categories[item.category] ?? RoomCategory.other(context); RoomCategory.other(widget.room?.serverTag ?? "",
widget.room?.id ?? "", context);
return ShoppingListItem( return ShoppingListItem(
name: item.name, name: item.name,
server: widget.room!.serverTag,
room: widget.room!.id,
description: item.description, description: item.description,
category: cat, category: cat,
key: Key(item.id.toString()), key: Key(item.id.toString()),
@ -253,10 +376,13 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = cart[index]; final item = cart[index];
final cat = final cat = categories[item.category] ??
categories[item.category] ?? RoomCategory.other(context); RoomCategory.other(widget.room!.serverTag,
widget.room!.id, context);
return ShoppingListItem( return ShoppingListItem(
server: widget.room!.serverTag,
room: widget.room!.id,
name: item.name, name: item.name,
description: item.description, description: item.description,
category: cat, category: cat,
@ -297,7 +423,7 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
}); });
}, },
) )
]), ])))),
floatingActionButton: (widget.info != null && floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) || ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||
@ -329,12 +455,16 @@ class ShoppingListItem extends StatelessWidget {
final Key _key; final Key _key;
final Function()? onDismiss; final Function()? onDismiss;
final Function()? onTap; final Function()? onTap;
final String server;
final String room;
const ShoppingListItem( const ShoppingListItem(
{required this.name, {required this.name,
required this.category, required this.category,
required this.inCart, required this.inCart,
required this.description, required this.description,
required this.server,
required this.room,
required key, required key,
this.onDismiss, this.onDismiss,
this.onTap}) this.onTap})
@ -367,6 +497,8 @@ class ShoppingListItem extends StatelessWidget {
title: Text(name), title: Text(name),
subtitle: Text(description), subtitle: Text(description),
trailing: CategoryChip( trailing: CategoryChip(
server: server,
room: room,
category: category, category: category,
), ),
onTap: () { onTap: () {
@ -410,6 +542,8 @@ class ShoppingListItemInfo extends StatelessWidget {
Text(item.name, style: textTheme.headlineLarge), Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium), Text(item.description, style: textTheme.titleMedium),
CategoryChip( CategoryChip(
server: server,
room: room,
category: category, category: category,
), ),
Text(Unit.fromId(item.unit).display(context, item.value)) Text(Unit.fromId(item.unit).display(context, item.value))
@ -445,11 +579,14 @@ class ShoppingListItemInfo extends StatelessWidget {
subtitle: Text(AppLocalizations.of(context)!.editItemLong), subtitle: Text(AppLocalizations.of(context)!.editItemLong),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
context.pushNamed('edit-product', params: { context.pushNamed('edit-item', params: {
'server': server, 'server': server,
'id': room, 'id': room,
'item': item.id.toString() 'item': item.id.toString()
}); });
final navInner = Navigator.of(context);
navInner.pop();
}, },
), ),
ListTile( ListTile(
@ -459,16 +596,70 @@ class ShoppingListItemInfo extends StatelessWidget {
AppLocalizations.of(context)!.deleteItemSubtitle), AppLocalizations.of(context)!.deleteItemSubtitle),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { 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( ListTile(
title: Text(item.state == 0 title: Text(item.state == 0
? AppLocalizations.of(context)!.moveItemToCartTitle ? AppLocalizations.of(context)!.moveItemToCartTitle
: AppLocalizations.of(context)!.moveItemToCartSubtitle), : AppLocalizations.of(context)!.removeItemFromCartTitle),
subtitle: Text(item.state == 0 subtitle: Text(item.state == 0
? AppLocalizations.of(context)!.removeItemFromCartTitle ? AppLocalizations.of(context)!.moveItemToCartSubtitle
: AppLocalizations.of(context)!.removeItemFromCartSubtitle), : AppLocalizations.of(context)!.removeItemFromCartSubtitle),
onTap: () { onTap: () {
// flip state // flip state
@ -484,7 +675,12 @@ class ShoppingListItemInfo extends StatelessWidget {
'server': server, 'server': server,
'listItemID': item.id, 'listItemID': item.id,
'state': item.state '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> { class _RoomProductsPageState extends State<RoomProductsPage> {
List<RoomProduct> products = []; List<RoomProduct> products = [];
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -32,10 +42,13 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}), body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) => RoomProduct.fromJSON(
widget.room!.serverTag, widget.room!.id, raw))
.toList(); .toList();
// TODO: cache products for (RoomProduct prod in resp) {
prod.toDisk();
}
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -49,13 +62,30 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
void initState() { void initState() {
super.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()); WidgetsBinding.instance.addPostFrameCallback((_) => fetchProducts());
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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, itemCount: products.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = products[index]; final item = products[index];
@ -80,7 +110,7 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
}, },
); );
}, },
), )))),
floatingActionButton: (widget.info != null && floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) || ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||

View file

@ -36,12 +36,19 @@ class _EditProductPageState extends State<EditProductPage> {
List<RoomCategory> categories = []; List<RoomCategory> categories = [];
List<RoomProduct> products = []; List<RoomProduct> products = [];
void fetchCategories() { void fetchCategories() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -49,7 +56,8 @@ class _EditProductPageState extends State<EditProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
setState(() { setState(() {
@ -58,12 +66,19 @@ class _EditProductPageState extends State<EditProductPage> {
}); });
} }
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -71,7 +86,8 @@ class _EditProductPageState extends State<EditProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
if (widget.product != null) { if (widget.product != null) {
@ -102,6 +118,16 @@ class _EditProductPageState extends State<EditProductPage> {
void initState() { void initState() {
super.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((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories(); fetchCategories();
fetchProducts(); fetchProducts();
@ -192,6 +218,8 @@ class _EditProductPageState extends State<EditProductPage> {
}, },
), ),
CategoryPicker( CategoryPicker(
server: widget.server,
room: widget.room,
label: AppLocalizations.of(context)! label: AppLocalizations.of(context)!
.selectCategoryLabel, .selectCategoryLabel,
hint: AppLocalizations.of(context)! hint: AppLocalizations.of(context)!
@ -252,7 +280,22 @@ class _EditProductPageState extends State<EditProductPage> {
'ean': _ctrEAN.text, 'ean': _ctrEAN.text,
'parent': _ctrParent '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(); nav.pop();
}); });
} else { } else {
@ -274,6 +317,20 @@ class _EditProductPageState extends State<EditProductPage> {
'parent': _ctrParent 'parent': _ctrParent
}), }),
onOK: (_) async { 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(); nav.pop();
}); });
} }

View file

@ -53,12 +53,24 @@ class _ViewProductPageState extends State<ViewProductPage> {
); );
} }
void fetchCategories() { void fetchCategories() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -66,7 +78,8 @@ class _ViewProductPageState extends State<ViewProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw)) .map<RoomCategory>((raw) =>
RoomCategory.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
Map<int?, RoomCategory> map = {}; Map<int?, RoomCategory> map = {};
@ -80,12 +93,19 @@ class _ViewProductPageState extends State<ViewProductPage> {
}); });
} }
void fetchProducts() { void fetchProducts() async {
final user = context.read<User>(); 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( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
@ -93,7 +113,8 @@ class _ViewProductPageState extends State<ViewProductPage> {
body: {'room': widget.room, 'server': widget.server}), body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async { onOK: (body) async {
final resp = body['data'] final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw)) .map<RoomProduct>((raw) =>
RoomProduct.fromJSON(widget.server, widget.room, raw))
.toList(); .toList();
for (RoomProduct prod in resp) { for (RoomProduct prod in resp) {
@ -115,6 +136,39 @@ class _ViewProductPageState extends State<ViewProductPage> {
void initState() { void initState() {
super.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((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories(); fetchCategories();
fetchProducts(); fetchProducts();
@ -131,6 +185,11 @@ class _ViewProductPageState extends State<ViewProductPage> {
title: Text(product?.name ?? ''), title: Text(product?.name ?? ''),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(children: [ child: Column(children: [
// display product into // display product into
Center( Center(
@ -140,14 +199,20 @@ class _ViewProductPageState extends State<ViewProductPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text(product?.name ?? '', style: textTheme.headlineLarge), Text(product?.name ?? '',
style: textTheme.headlineLarge),
Text(product?.description ?? '', Text(product?.description ?? '',
style: textTheme.titleMedium), style: textTheme.titleMedium),
Text(product?.ean ?? ''), Text(product?.ean ?? ''),
CategoryChip(category: categories[product?.category]), CategoryChip(
server: widget.server,
room: widget.room,
category:
categories[product?.category]),
Text(product != null Text(product != null
? Unit.fromId(product!.defaultUnit) ? Unit.fromId(product!.defaultUnit)
.display(context, product!.defaultValue) .display(
context, product!.defaultValue)
: '') : '')
], ],
))), ))),
@ -157,12 +222,15 @@ class _ViewProductPageState extends State<ViewProductPage> {
...(info != null && ...(info != null &&
(info!.isAdmin || (info!.isAdmin ||
info!.isOwner || info!.isOwner ||
(info!.permissions & RoomPermission.editRoomContent != 0))) (info!.permissions &
RoomPermission.editRoomContent !=
0)))
? [ ? [
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.editProductTitle), title: Text(AppLocalizations.of(context)!
subtitle: .editProductTitle),
Text(AppLocalizations.of(context)!.editProductSubtitle), subtitle: Text(AppLocalizations.of(context)!
.editProductSubtitle),
onTap: () { onTap: () {
context.pushNamed('edit-product', params: { context.pushNamed('edit-product', params: {
'server': widget.server, 'server': widget.server,
@ -178,10 +246,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
...(product?.parent != null) ...(product?.parent != null)
? [ ? [
ListTile( ListTile(
title: Text( title: Text(AppLocalizations.of(context)!
AppLocalizations.of(context)!.viewParentProductTitle), .viewParentProductTitle),
subtitle: Text( subtitle: Text(AppLocalizations.of(context)!
AppLocalizations.of(context)!.viewParentProductSubtitle), .viewParentProductSubtitle),
onTap: () { onTap: () {
context.pushNamed('view-product', params: { context.pushNamed('view-product', params: {
'server': widget.server, 'server': widget.server,
@ -195,9 +263,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
: [], : [],
// show/manage children // show/manage children
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.viewProductChildrenTitle), title: Text(AppLocalizations.of(context)!
subtitle: .viewProductChildrenTitle),
Text(AppLocalizations.of(context)!.viewProductChildrenSubtitle), subtitle: Text(AppLocalizations.of(context)!
.viewProductChildrenSubtitle),
onTap: () { onTap: () {
context.pushNamed('view-product-children', params: { context.pushNamed('view-product-children', params: {
'server': widget.server, 'server': widget.server,
@ -207,7 +276,101 @@ class _ViewProductPageState extends State<ViewProductPage> {
}, },
trailing: const Icon(Icons.chevron_right), 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) { if (after != null) {
after(); await after();
} }
} }

View file

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

View file

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