From e9ae8be54dff02cb542f6c992d0e6a1dfbdb9c3a Mon Sep 17 00:00:00 2001 From: Lu Shueh Chou Date: Sun, 16 Jun 2024 13:31:18 +0800 Subject: [PATCH 1/5] using system language as default value --- lib/components/style/date_range_picker.dart | 2 +- lib/my_app.dart | 3 ++- lib/settings/currency_setting.dart | 4 +-- lib/settings/language_setting.dart | 27 +++++++++++-------- .../widgets/history_calendar_view.dart | 2 +- lib/ui/home/features_page.dart | 4 +-- lib/ui/home/setting_view.dart | 2 +- test/settings/language_setting_test.dart | 2 +- test/ui/home/features_page_test.dart | 2 +- .../google_sheet/export_order_test.dart | 4 +-- 10 files changed, 29 insertions(+), 23 deletions(-) diff --git a/lib/components/style/date_range_picker.dart b/lib/components/style/date_range_picker.dart index 07fc4e40..bb7a2f75 100644 --- a/lib/components/style/date_range_picker.dart +++ b/lib/components/style/date_range_picker.dart @@ -16,7 +16,7 @@ Future showMyDateRangePicker(BuildContext context, DateTimeRange initialEntryMode: DatePickerEntryMode.calendarOnly, firstDate: DateTime(2021, 1), lastDate: DateTime.now(), - locale: LanguageSetting.instance.value.locale, + locale: LanguageSetting.instance.language.locale, /// TODO: should fix this bug /// Wrapping the design, because the background will use a slightly diff --git a/lib/my_app.dart b/lib/my_app.dart index 974465a0..91f78702 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -57,6 +57,7 @@ class MyApp extends StatelessWidget { S = localizations; Intl.systemLocale = S.localeName; Intl.defaultLocale = S.localeName; + LanguageSetting.instance.systemLanguage = S.localeName; initializeDateFormatting(S.localeName); FlutterNativeSplash.remove(); @@ -68,7 +69,7 @@ class MyApp extends StatelessWidget { // Provide the generated AppLocalizations to the MaterialApp. This // allows descendant Widgets to display the correct translations // depending on the user's locale. - locale: LanguageSetting.instance.value.locale, + locale: LanguageSetting.instance.value?.locale, supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/lib/settings/currency_setting.dart b/lib/settings/currency_setting.dart index d19cffc6..6801850b 100644 --- a/lib/settings/currency_setting.dart +++ b/lib/settings/currency_setting.dart @@ -24,7 +24,7 @@ class CurrencySetting extends Setting { CurrencySetting._() { value = defaultValue; LanguageSetting.instance.addListener(() { - formatter = NumberFormat.compact(locale: LanguageSetting.instance.value.locale.toString()); + formatter = NumberFormat.compact(locale: LanguageSetting.instance.language.locale.toString()); }); } @@ -33,7 +33,7 @@ class CurrencySetting extends Setting { String get recordName => '新台幣'; - NumberFormat formatter = NumberFormat.compact(locale: LanguageSetting.instance.value.locale.toString()); + NumberFormat formatter = NumberFormat.compact(locale: LanguageSetting.instance.language.locale.toString()); /// Ceiling [value] to currency least value /// diff --git a/lib/settings/language_setting.dart b/lib/settings/language_setting.dart index b89682d2..9dd4b97b 100644 --- a/lib/settings/language_setting.dart +++ b/lib/settings/language_setting.dart @@ -1,15 +1,17 @@ import 'dart:ui'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:possystem/settings/setting.dart'; -class LanguageSetting extends Setting { - static final instance = LanguageSetting._(); +/// Language setting allow given null language which means system default. +class LanguageSetting extends Setting { + Language _systemLanguage = Language.en; - static const defaultValue = Language.en; + static final instance = LanguageSetting._(); LanguageSetting._() { - value = defaultValue; + value = Language.en; } @override @@ -18,15 +20,21 @@ class LanguageSetting extends Setting { @override bool get registryForApp => true; + set systemLanguage(String locale) { + _systemLanguage = parseLanguage(locale)!; + } + + Language get language => value ?? _systemLanguage; + @override void initialize() { - value = parseLanguage(service.get(key)) ?? defaultValue; + value = parseLanguage(service.get(key)); notifyListeners(); } @override - Future updateRemotely(Language data) { - return service.set(key, data.locale.toString()); + Future updateRemotely(Language? data) { + return service.set(key, data?.locale.toString() ?? ''); } Language? parseLanguage(String? value) { @@ -34,10 +42,7 @@ class LanguageSetting extends Setting { final codes = value.split('_'); - return Language.values.firstWhere( - (e) => e.locale.languageCode == codes[0], - orElse: () => defaultValue, - ); + return Language.values.firstWhereOrNull((e) => e.locale.languageCode == codes[0]); } } diff --git a/lib/ui/analysis/widgets/history_calendar_view.dart b/lib/ui/analysis/widgets/history_calendar_view.dart index 54915535..620faef9 100644 --- a/lib/ui/analysis/widgets/history_calendar_view.dart +++ b/lib/ui/analysis/widgets/history_calendar_view.dart @@ -52,7 +52,7 @@ class _HistoryCalendarViewState extends State { shouldFillViewport: widget.isPortrait ? false : true, startingDayOfWeek: StartingDayOfWeek.monday, rangeSelectionMode: RangeSelectionMode.disabled, - locale: LanguageSetting.instance.value.locale.toString(), + locale: LanguageSetting.instance.language.locale.toString(), // header // chinese will be hidden if using default value daysOfWeekHeight: 20.0, diff --git a/lib/ui/home/features_page.dart b/lib/ui/home/features_page.dart index da51a1d3..3e4c9791 100644 --- a/lib/ui/home/features_page.dart +++ b/lib/ui/home/features_page.dart @@ -81,7 +81,7 @@ class FeaturesPage extends StatelessWidget { key: const Key('feature.language'), leading: const Icon(Icons.language_outlined), title: Text(S.settingLanguageTitle), - subtitle: Text(LanguageSetting.instance.value.title), + subtitle: Text(LanguageSetting.instance.language.title), trailing: const Icon(Icons.arrow_forward_ios_sharp), onTap: () => navigateTo(Feature.language), ), @@ -229,7 +229,7 @@ enum Feature { case Feature.theme: return ThemeSetting.instance.value.index; case Feature.language: - return LanguageSetting.instance.value.index; + return LanguageSetting.instance.language.index; case Feature.orderOutlook: return OrderOutlookSetting.instance.value.index; case Feature.checkoutWarning: diff --git a/lib/ui/home/setting_view.dart b/lib/ui/home/setting_view.dart index 2753c43a..6377008c 100644 --- a/lib/ui/home/setting_view.dart +++ b/lib/ui/home/setting_view.dart @@ -229,7 +229,7 @@ class _HeaderInfoList extends StatelessWidget { ), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title.toString(), style: theme.textTheme.headlineMedium), - Text(subtitle, style: theme.textTheme.bodyMedium), + Text(subtitle, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center), ]), ), ); diff --git a/test/settings/language_setting_test.dart b/test/settings/language_setting_test.dart index 1019682d..9367096a 100644 --- a/test/settings/language_setting_test.dart +++ b/test/settings/language_setting_test.dart @@ -6,7 +6,7 @@ void main() { test('Parse language', () { final l = LanguageSetting.instance; expect(l.parseLanguage(''), isNull); - expect(l.parseLanguage('something'), equals(Language.en)); + expect(l.parseLanguage('something'), equals(null)); expect(l.parseLanguage('zh'), equals(Language.zhTW)); expect(l.parseLanguage('zh_TW'), equals(Language.zhTW)); expect(l.parseLanguage('zh_Hant'), equals(Language.zhTW)); diff --git a/test/ui/home/features_page_test.dart b/test/ui/home/features_page_test.dart index 4ccf0ab2..866f3dad 100644 --- a/test/ui/home/features_page_test.dart +++ b/test/ui/home/features_page_test.dart @@ -22,7 +22,7 @@ void main() { return ChangeNotifierProvider.value( value: SettingsProvider.instance..initialize(), builder: (_, __) => MaterialApp.router( - locale: LanguageSetting.defaultValue.locale, + locale: LanguageSetting.instance.language.locale, routerConfig: GoRouter(initialLocation: Routes.features, routes: [ GoRoute( path: '/', diff --git a/test/ui/transit/google_sheet/export_order_test.dart b/test/ui/transit/google_sheet/export_order_test.dart index 78581257..fd35cc61 100644 --- a/test/ui/transit/google_sheet/export_order_test.dart +++ b/test/ui/transit/google_sheet/export_order_test.dart @@ -63,13 +63,13 @@ void main() { ); await tester.pumpWidget(MaterialApp( - locale: LanguageSetting.defaultValue.locale, + locale: LanguageSetting.instance.language.locale, localizationsDelegates: const >[ DefaultWidgetsLocalizations.delegate, DefaultMaterialLocalizations.delegate, DefaultCupertinoLocalizations.delegate, ], - supportedLocales: [LanguageSetting.defaultValue.locale], + supportedLocales: [LanguageSetting.instance.language.locale], home: TransitStation( catalog: TransitCatalog.order, method: TransitMethod.googleSheet, From 1f6de68a5177e82f3f9b2ac96a87385c2e8502d1 Mon Sep 17 00:00:00 2001 From: Lu Shueh Chou Date: Sat, 22 Jun 2024 10:39:35 +0800 Subject: [PATCH 2/5] allow setup menu from tutorial --- assets/l10n/en/menu.yaml | 34 ++- assets/l10n/en/order.yaml | 5 + assets/l10n/en/transit.yaml | 4 +- assets/l10n/zh/menu.yaml | 34 ++- assets/l10n/zh/order.yaml | 5 + assets/l10n/zh/transit.yaml | 4 +- lib/components/tutorial.dart | 49 ++++- lib/debug/setup_menu.dart | 100 --------- lib/helpers/setup_menu.dart | 210 +++++++++++++++++++ lib/l10n/app_en.arb | 36 +++- lib/l10n/app_zh.arb | 36 +++- lib/main.dart | 5 - lib/models/objects/menu_object.dart | 6 +- lib/ui/home/setting_view.dart | 208 +++++++++++------- lib/ui/menu/menu_page.dart | 25 +-- pubspec.lock | 4 +- pubspec.yaml | 2 +- test/{debug => helpers}/setup_menu_test.dart | 8 +- 18 files changed, 537 insertions(+), 238 deletions(-) delete mode 100644 lib/debug/setup_menu.dart create mode 100644 lib/helpers/setup_menu.dart rename test/{debug => helpers}/setup_menu_test.dart (80%) diff --git a/assets/l10n/en/menu.yaml b/assets/l10n/en/menu.yaml index 976260eb..9b5afc1d 100644 --- a/assets/l10n/en/menu.yaml +++ b/assets/l10n/en/menu.yaml @@ -4,15 +4,45 @@ subtitle: Categories, Products tutorial: title: Create Your Menu content: Let's start by creating a menu! + createExample: Help create an example menu to test. search: hint: Search for products, ingredients, quantities notFound: Couldn't find relevant information. Did you misspell something? +example: + catalog: + burger: '🍔 Burgers' + drink: '🍻 Drinks' + side: '🍰 Side' + other: '🛍 Others' + product: + cheeseBurger: Cheese Burger + veggieBurger: Veggie Burger + hamBurger: Ham Burger + cola: Cola + coffee: Coffee + fries: Fries + straw: Straw + plasticBag: Plastic Bag + ingredient: + cheese: '🧀 Cheese' + lettuce: '🥬 Lettuce' + tomato: '🍅 Tomato' + bun: '🍞 Bun' + chili: '🌶 Chili Sauce' + ham: '🍖 Ham' + cola: '🥤 Can of Cola' + coffee: '☕️ Drip Coffee' + fries: '🍟 Bag of Fries' + straw: 'Straw' + plasticBag: 'Plastic Bag' + quantity: + small: Small + large: Large + none: None catalog: headerInfo: - Categories - Displayed on the upper rectangle in homepage - tutorial: - title: Create First Catalog emptyBody: |- Similar "products" will be grouped under "categories", making it convenient for ordering, such as: diff --git a/assets/l10n/en/order.yaml b/assets/l10n/en/order.yaml index fadf5984..c5d497e4 100644 --- a/assets/l10n/en/order.yaml +++ b/assets/l10n/en/order.yaml @@ -1,6 +1,11 @@ $prefix: order title: Ordering btn: Order +tutorial: + title: Ordering! + content: | + Once you have set up your menu, you can start ordering! + Let's tap and go see what's available! snackbar: cashier: notEnough: Insufficient cash in the cashier! diff --git a/assets/l10n/en/transit.yaml b/assets/l10n/en/transit.yaml index 4fc551ff..07d68766 100644 --- a/assets/l10n/en/transit.yaml +++ b/assets/l10n/en/transit.yaml @@ -67,7 +67,7 @@ export: preview: btn: Preview title: Preview Output Result - btn: Import + btn: Export import: preview: btn: Preview @@ -86,7 +86,7 @@ import: header: After import, old ingredients won't be removed to avoid affecting the "Menu" status. quantity: header: After import, old quantities won't be removed to avoid affecting the "Menu" status. - btn: Export + btn: Import error: columnCount: - Insufficient data, {columns} columns required diff --git a/assets/l10n/zh/menu.yaml b/assets/l10n/zh/menu.yaml index 34332f4a..bcecf2bc 100644 --- a/assets/l10n/zh/menu.yaml +++ b/assets/l10n/zh/menu.yaml @@ -4,13 +4,43 @@ subtitle: 產品種類、產品 tutorial: title: 建立屬於你的菜單 content: 首先我們來開始建立一份菜單吧! + createExample: 幫助建立一份範例菜單以供測試。 search: hint: 搜尋產品、成分、份量 notFound: 搜尋不到相關資訊,打錯字了嗎? +example: + catalog: + burger: '🍔 漢堡' + drink: '🍻 飲品' + side: '🍰 點心' + other: '🛍 其他' + product: + cheeseBurger: 起司漢堡 + veggieBurger: 蔬菜漢堡 + hamBurger: 火腿漢堡 + cola: 可樂 + coffee: 咖啡 + fries: 薯條 + straw: 吸管 + plasticBag: 塑膠袋 + ingredient: + cheese: '🧀 起司' + lettuce: '🥬 萵苣' + tomato: '🍅 番茄' + bun: '🍞 麵包' + chili: '🌶 醬料' + ham: '🍖 火腿' + cola: '🥤 可樂罐' + coffee: '☕️ 濾掛咖啡包' + fries: '🍟 薯條(300g)' + straw: 吸管(根) + plasticBag: 塑膠袋(個) + quantity: + small: 少量 + large: 增量 + none: 無 catalog: headerInfo: 種類 - tutorial: - title: Create First Catalog emptyBody: |- 我們會把相似「產品」放在「產品種類」中, 到時候點餐會比較方便,例如: diff --git a/assets/l10n/zh/order.yaml b/assets/l10n/zh/order.yaml index 5337292b..c3267677 100644 --- a/assets/l10n/zh/order.yaml +++ b/assets/l10n/zh/order.yaml @@ -1,6 +1,11 @@ $prefix: order title: 點餐 btn: 點餐 +tutorial: + title: 開始點餐! + content: | + 一旦設定好菜單,就可以開始點餐囉 + 讓我們趕緊進去看看有什麼吧! snackbar: cashier: notEnough: 收銀機錢不夠找囉! diff --git a/assets/l10n/zh/transit.yaml b/assets/l10n/zh/transit.yaml index b861a1de..56a1d67f 100644 --- a/assets/l10n/zh/transit.yaml +++ b/assets/l10n/zh/transit.yaml @@ -51,7 +51,7 @@ export: preview: btn: 預覽 title: 預覽輸出結果 - btn: 匯入 + btn: 匯出 import: preview: btn: 預覽 @@ -66,7 +66,7 @@ import: header: 匯入後,為了避免影響「菜單」的狀況,並不會把舊的成分移除。 quantity: header: 匯入後,為了避免影響「菜單」的狀況,並不會把舊的份量移除。 - btn: 匯出 + btn: 匯入 error: columnCount: 資料量不足,需要 {columns} 個欄位 duplicate: 將忽略本行,相同的項目已於前面出現 diff --git a/lib/components/tutorial.dart b/lib/components/tutorial.dart index 74e3c792..21d09a9e 100644 --- a/lib/components/tutorial.dart +++ b/lib/components/tutorial.dart @@ -31,12 +31,19 @@ class TutorialWrapper extends StatelessWidget { class Tutorial extends StatelessWidget { final String id; - final String? title; - + /// index of the tutorial + /// + /// 0-based index, if not provided, the tutorial will be ordered by + /// the time of the widget built. final int? index; + final String? title; + final String message; + /// widget to be placed below the [message] + final Widget? below; + final SpotlightBuilder spotlightBuilder; final EdgeInsets padding; @@ -44,25 +51,36 @@ class Tutorial extends StatelessWidget { /// force disabling tutorial final bool disable; + /// if true, the tutorial will only be shown when the widget is 100% visible final bool monitorVisibility; final Widget child; final SpotlightDurationConfig duration; + /// route to be pushed after the tutorial is dismissed + /// + /// if [action] is provided, this will be ignored final String? route; + /// action to be executed after the tutorial is dismissed + final Future Function()? action; + + final bool _hasAction; + const Tutorial({ super.key, required this.id, + this.index, this.title, required this.message, - this.index, + this.below, this.spotlightBuilder = const SpotlightCircularBuilder(), this.padding = const EdgeInsets.all(8), this.disable = false, this.monitorVisibility = false, this.route, + this.action, required this.child, this.duration = const SpotlightDurationConfig( bump: Duration(milliseconds: 500), @@ -70,7 +88,7 @@ class Tutorial extends StatelessWidget { zoomOut: Duration(milliseconds: 600), contentFadeIn: Duration(milliseconds: 200), ), - }); + }) : _hasAction = route != null || action != null; @override Widget build(BuildContext context) { @@ -78,31 +96,40 @@ class Tutorial extends StatelessWidget { return child; } + final goSkip = _hasAction ? () => SpotlightAntAction.skip : null; return SpotlightAnt( enable: enabled, index: index, duration: duration, monitorId: monitorVisibility ? 'tutorial.$id' : null, onDismiss: _onDismiss, - onDismissed: route != null ? () => context.goNamed(route!) : null, + onDismissed: _hasAction + ? () async { + await (action?.call() ?? context.pushNamed(route!)); + + // try start the next tutorial, if exists + if (context.mounted) { + SpotlightShow.maybeOf(context)?.start(); + } + } + : null, spotlight: SpotlightConfig( builder: spotlightBuilder, padding: padding, - onTap: route != null ? () async => SpotlightAntAction.skip : null, + onTap: goSkip, ), - backdrop: SpotlightBackdropConfig(silent: route != null), + backdrop: SpotlightBackdropConfig(onTap: goSkip), action: const SpotlightActionConfig( enabled: [SpotlightAntAction.prev, SpotlightAntAction.next], ), content: SpotlightContent( child: Column(children: [ if (title != null) - Text( - title!, - style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), - ), + // headline medium style + Text(title!, style: const TextStyle(fontSize: 34, fontWeight: FontWeight.bold, wordSpacing: 0.25)), const SizedBox(height: 16), Text(message), + if (below != null) below!, ]), ), child: child, diff --git a/lib/debug/setup_menu.dart b/lib/debug/setup_menu.dart deleted file mode 100644 index 210946df..00000000 --- a/lib/debug/setup_menu.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:developer'; - -import 'package:possystem/models/menu/catalog.dart'; -import 'package:possystem/models/objects/menu_object.dart'; -import 'package:possystem/models/repository/menu.dart'; -import 'package:possystem/models/repository/quantities.dart'; -import 'package:possystem/models/repository/stock.dart'; -import 'package:possystem/models/stock/ingredient.dart'; -import 'package:possystem/models/stock/quantity.dart'; - -Future debugSetupMenu() async { - if (Menu.instance.isNotEmpty) return; - - log('DEBUG setup stock'); - await Stock.instance.addItem(Ingredient( - id: 'cheese', - name: 'Cheese', - currentAmount: 30, - )); - await Stock.instance.addItem(Ingredient( - id: 'vegetable', - name: 'Vegetable', - currentAmount: 100, - )); - await Stock.instance.addItem(Ingredient( - id: 'bread', - name: 'Bread', - currentAmount: 50, - )); - - log('DEBUG setup quantities'); - await Quantities.instance.addItem(Quantity( - id: 'more', - name: 'More', - defaultProportion: 1.5, - )); - await Quantities.instance.addItem(Quantity( - id: 'less', - name: 'Less', - defaultProportion: 0.8, - )); - - log('DEBUG setup menu'); - await Menu.instance.addItem(Catalog.fromObject(CatalogObject.build({ - "id": "burger", - "index": 1, - "name": "Burger", - "createdAt": 1648885177, - "imagePath": null, - "products": { - "cheese-burger": { - "price": 60, - "cost": 55, - "index": 1, - "name": "Cheese Burger", - "imagePath": null, - "createdAt": 1648885807, - "ingredients": { - "cb-ingredient1": { - "ingredientId": "cheese", - "amount": 18, - "quantities": { - "cb-quantity-1": {"quantityId": "more", "amount": 18, "additionalCost": 5, "additionalPrice": 15}, - "cb-quantity-2": {"quantityId": "less", "amount": 9.0, "additionalCost": 0, "additionalPrice": 0} - } - }, - "cb-ingredient2": {"ingredientId": "bread", "amount": 15, "quantities": {}} - } - }, - "veg-burger": { - "price": 50, - "cost": 30, - "index": 2, - "name": "Veg Burger", - "imagePath": null, - "createdAt": 1648885992, - "ingredients": { - "vb-ingredient1": { - "ingredientId": "vegetable", - "amount": 10, - "quantities": { - "vb-quantity1": {"quantityId": "more", "amount": 20, "additionalCost": 2, "additionalPrice": 8}, - "vb-quantity2": {"quantityId": "less", "amount": 5.0, "additionalCost": 0, "additionalPrice": 0} - } - }, - "vb-ingredient2": {"ingredientId": "bread", "amount": 15, "quantities": {}} - } - }, - "rice-burger": { - "price": 88, - "cost": 60, - "index": 3, - "name": "Rice Burger", - "imagePath": null, - "createdAt": 1648886087, - "ingredients": {} - } - } - }))); -} diff --git a/lib/helpers/setup_menu.dart b/lib/helpers/setup_menu.dart new file mode 100644 index 00000000..3719d67a --- /dev/null +++ b/lib/helpers/setup_menu.dart @@ -0,0 +1,210 @@ +import 'dart:developer'; + +import 'package:possystem/models/menu/catalog.dart'; +import 'package:possystem/models/objects/menu_object.dart'; +import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/quantities.dart'; +import 'package:possystem/models/repository/stock.dart'; +import 'package:possystem/models/stock/ingredient.dart'; +import 'package:possystem/models/stock/quantity.dart'; +import 'package:possystem/translator.dart'; + +Future helpSetupMenu() async { + if (Menu.instance.isNotEmpty) return; + + log('setting stock', name: 'example menu'); + for (final e in [ + Ingredient(id: 'cheese', name: S.menuExampleIngredientCheese, currentAmount: 30, totalAmount: 30), + Ingredient(id: 'lettuce', name: S.menuExampleIngredientLettuce, currentAmount: 70, totalAmount: 70), + Ingredient(id: 'tomato', name: S.menuExampleIngredientTomato, currentAmount: 100, totalAmount: 100), + Ingredient(id: 'bun', name: S.menuExampleIngredientBun, currentAmount: 50, totalAmount: 50), + Ingredient(id: 'chili', name: S.menuExampleIngredientChili, currentAmount: 500, totalAmount: 500), + Ingredient(id: 'ham', name: S.menuExampleIngredientHam, currentAmount: 5, totalAmount: 5), + Ingredient(id: 'cola', name: S.menuExampleIngredientCola, currentAmount: 20, totalAmount: 20), + Ingredient(id: 'coffee', name: S.menuExampleIngredientCoffee, currentAmount: 50, totalAmount: 50), + Ingredient(id: 'fries', name: S.menuExampleIngredientFries, currentAmount: 3, totalAmount: 3), + Ingredient(id: 'straw', name: S.menuExampleIngredientStraw, currentAmount: 50, totalAmount: 50), + Ingredient(id: 'plasticBag', name: S.menuExampleIngredientPlasticBag, currentAmount: 50, totalAmount: 50), + ]) { + await Stock.instance.addItem(e); + } + + log('setting quantities', name: 'example menu'); + for (final e in [ + Quantity(id: 'none', name: S.menuExampleQuantityNone, defaultProportion: 0), + Quantity(id: 'small', name: S.menuExampleQuantitySmall, defaultProportion: 0.5), + Quantity(id: 'large', name: S.menuExampleQuantityLarge, defaultProportion: 1.5), + ]) { + await Quantities.instance.addItem(e); + } + + log('setting menu', name: 'example menu'); + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + for (final e in [ + Catalog.fromObject(CatalogObject.build({ + "id": "burger", + "index": 1, + "name": S.menuExampleCatalogBurger, + "createdAt": now, + "products": { + "cheese-burger": { + "price": 80, + "cost": 40, + "index": 1, + "name": S.menuExampleProductCheeseBurger, + "createdAt": now, + "ingredients": { + "cb-ingredient1": { + "ingredientId": "cheese", + "amount": 0.3, + "quantities": { + "cb1-quantity1": {"quantityId": "large", "amount": 0.5, "additionalCost": 5, "additionalPrice": 10}, + "cb1-quantity2": {"quantityId": "small", "amount": 0.1}, + } + }, + "cb-ingredient2": {"ingredientId": "bun", "amount": 1}, + "cb-ingredient3": { + "ingredientId": "lettuce", + "amount": 1, + "quantities": { + "cb3-quantity2": {"quantityId": "none", "amount": 0}, + } + }, + }, + }, + "veg-burger": { + "price": 60, + "cost": 30, + "index": 2, + "name": S.menuExampleProductVeggieBurger, + "createdAt": now, + "ingredients": { + "vb-ingredient1": { + "ingredientId": "tomato", + "amount": 0.2, + "quantities": { + "vb1-quantity1": {"quantityId": "more", "amount": 0.5, "additionalCost": 2, "additionalPrice": 5}, + "vb1-quantity2": {"quantityId": "less", "amount": 0.1}, + }, + }, + "vb-ingredient2": {"ingredientId": "bun", "amount": 1}, + "vb-ingredient3": { + "ingredientId": "lettuce", + "amount": 1, + "quantities": { + "cb3-quantity2": {"quantityId": "none", "amount": 0}, + } + }, + } + }, + "ham-burger": { + "price": 100, + "cost": 50, + "index": 3, + "name": S.menuExampleProductHamBurger, + "createdAt": now, + "ingredients": { + "hb-ingredient1": { + "ingredientId": "ham", + "amount": 0.3, + "quantities": { + "hb1-quantity1": {"quantityId": "more", "amount": 0.6, "additionalCost": 10, "additionalPrice": 30}, + }, + }, + "hb-ingredient2": {"ingredientId": "bun", "amount": 1}, + "hb-ingredient3": { + "ingredientId": "lettuce", + "amount": 1, + "quantities": { + "cb3-quantity2": {"quantityId": "none", "amount": 0}, + } + }, + } + }, + }, + })), + Catalog.fromObject(CatalogObject.build({ + "id": "drink", + "index": 2, + "name": S.menuExampleCatalogDrink, + "createdAt": now, + "products": { + "cola": { + "price": 30, + "cost": 20, + "index": 1, + "name": S.menuExampleProductCola, + "createdAt": now, + "ingredients": { + "cola-ingredient1": {"ingredientId": "cola", "amount": 1}, + "coffee-ingredient2": {"ingredientId": "straw", "amount": 1}, + }, + }, + "coffee": { + "price": 50, + "cost": 20, + "index": 2, + "name": S.menuExampleProductCoffee, + "createdAt": now, + "ingredients": { + "coffee-ingredient1": {"ingredientId": "coffee", "amount": 1}, + }, + }, + }, + })), + Catalog.fromObject(CatalogObject.build({ + "id": "side", + "index": 3, + "name": S.menuExampleCatalogSide, + "createdAt": now, + "products": { + "fries": { + "price": 50, + "cost": 25, + "index": 1, + "name": S.menuExampleProductFries, + "createdAt": now, + "ingredients": { + "fries-ingredient1": { + "ingredientId": "fries", + "amount": 0.1, + "quantities": { + "fries1-quantity1": {"quantityId": "more", "amount": 0.2, "additionalCost": 5, "additionalPrice": 10}, + }, + }, + }, + }, + }, + })), + Catalog.fromObject(CatalogObject.build({ + "id": "other", + "index": 4, + "name": S.menuExampleCatalogOther, + "createdAt": now, + "products": { + "plastic-bag": { + "price": 5, + "cost": 2, + "index": 1, + "name": S.menuExampleProductPlasticBag, + "createdAt": now, + "ingredients": { + "plastic-bag-ingredient1": {"ingredientId": "plasticBag", "amount": 1}, + }, + }, + "straw": { + "price": 5, + "cost": 0.1, + "index": 2, + "name": S.menuExampleProductStraw, + "createdAt": now, + "ingredients": { + "straw-ingredient1": {"ingredientId": "straw", "amount": 1}, + }, + }, + }, + })), + ]) { + await Menu.instance.addItem(e); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e8240938..238111a9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2024-06-02T09:08:47.831122Z", + "@@last_modified": "2024-06-22T02:38:59.210340Z", "@@author": "Lu Shueh Chou", "settingTab": "Settings", "settingVersion": "Version: {version}", @@ -306,7 +306,7 @@ "transitOrderItemDialogTitle": "Order Details", "transitExportPreviewBtn": "Preview", "transitExportPreviewTitle": "Preview Output Result", - "transitExportBtn": "Import", + "transitExportBtn": "Export", "transitImportPreviewBtn": "Preview", "transitImportPreviewTitle": "Preview Import Result", "transitImportPreviewHeader": "Note: Importing will remove the data not listed below. Please confirm before executing!", @@ -333,7 +333,7 @@ }, "transitImportPreviewIngredientHeader": "After import, old ingredients won't be removed to avoid affecting the \"Menu\" status.", "transitImportPreviewQuantityHeader": "After import, old quantities won't be removed to avoid affecting the \"Menu\" status.", - "transitImportBtn": "Export", + "transitImportBtn": "Import", "transitImportErrorColumnCount": "Insufficient data, {columns} columns required", "@transitImportErrorColumnCount": { "placeholders": { @@ -1277,13 +1277,39 @@ "menuSubtitle": "Categories, Products", "menuTutorialTitle": "Create Your Menu", "menuTutorialContent": "Let's start by creating a menu!", + "menuTutorialCreateExample": "Help create an example menu to test.", "menuSearchHint": "Search for products, ingredients, quantities", "menuSearchNotFound": "Couldn't find relevant information. Did you misspell something?", + "menuExampleCatalogBurger": "🍔 Burgers", + "menuExampleCatalogDrink": "🍻 Drinks", + "menuExampleCatalogSide": "🍰 Side", + "menuExampleCatalogOther": "🛍 Others", + "menuExampleProductCheeseBurger": "Cheese Burger", + "menuExampleProductVeggieBurger": "Veggie Burger", + "menuExampleProductHamBurger": "Ham Burger", + "menuExampleProductCola": "Cola", + "menuExampleProductCoffee": "Coffee", + "menuExampleProductFries": "Fries", + "menuExampleProductStraw": "Straw", + "menuExampleProductPlasticBag": "Plastic Bag", + "menuExampleIngredientCheese": "🧀 Cheese", + "menuExampleIngredientLettuce": "🥬 Lettuce", + "menuExampleIngredientTomato": "🍅 Tomato", + "menuExampleIngredientBun": "🍞 Bun", + "menuExampleIngredientChili": "🌶 Chili Sauce", + "menuExampleIngredientHam": "🍖 Ham", + "menuExampleIngredientCola": "🥤 Can of Cola", + "menuExampleIngredientCoffee": "☕️ Drip Coffee", + "menuExampleIngredientFries": "🍟 Bag of Fries", + "menuExampleIngredientStraw": "Straw", + "menuExampleIngredientPlasticBag": "Plastic Bag", + "menuExampleQuantitySmall": "Small", + "menuExampleQuantityLarge": "Large", + "menuExampleQuantityNone": "None", "menuCatalogHeaderInfo": "Categories", "@menuCatalogHeaderInfo": { "description": "Displayed on the upper rectangle in homepage" }, - "menuCatalogTutorialTitle": "Create First Catalog", "menuCatalogEmptyBody": "Similar \"products\" will be grouped under \"categories\",\nmaking it convenient for ordering, such as:\n• \"Cheese Burger\", \"Veggie Burger\" > \"Burgers\"\n• \"Plastic Bag\", \"Eco Cup\" > \"Others\"", "menuCatalogTitleCreate": "Add Category", "@menuCatalogTitleCreate": { @@ -1536,6 +1562,8 @@ "cashierSurplusDiffTotalHelper": "The difference from the total amount of the cash register at the very beginning.\nThis can quickly help you understand how much money the cash register has gained today.", "orderTitle": "Ordering", "orderBtn": "Order", + "orderTutorialTitle": "Ordering!", + "orderTutorialContent": "Once you have set up your menu, you can start ordering!\nLet's tap and go see what's available!\n", "orderSnackbarCashierNotEnough": "Insufficient cash in the cashier!", "orderSnackbarCashierUsingSmallMoney": "Using smaller denominations to give change", "orderSnackbarCashierUsingSmallMoneyHelper": "When giving change to customers, if the cashier doesn't have the appropriate denominations, this message will appear.\n\nFor example, if the total is $65 and the customer pays $100, the change should be $35.\nIf the cashier only has two $10 bills and more than three $5 bills, this message will appear.\n\nTo avoid this prompt:\n• Go to the changer page and top up various denominations.\n• Go to the [settings page]({link}) to disable related prompts from the cashier.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index dc7e7ff6..6da8ae50 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "@@last_modified": "2024-06-02T09:08:47.880851Z", + "@@last_modified": "2024-06-22T02:38:59.251648Z", "@@author": "Lu Shueh Chou", "settingTab": "設定", "settingVersion": "版本:{version}", @@ -306,7 +306,7 @@ "transitOrderItemDialogTitle": "訂單細節", "transitExportPreviewBtn": "預覽", "transitExportPreviewTitle": "預覽輸出結果", - "transitExportBtn": "匯入", + "transitExportBtn": "匯出", "transitImportPreviewBtn": "預覽", "transitImportPreviewTitle": "預覽匯入結果", "transitImportPreviewHeader": "注意:匯入後將會把下面沒列到的資料移除,請確認是否執行!", @@ -333,7 +333,7 @@ }, "transitImportPreviewIngredientHeader": "匯入後,為了避免影響「菜單」的狀況,並不會把舊的成分移除。", "transitImportPreviewQuantityHeader": "匯入後,為了避免影響「菜單」的狀況,並不會把舊的份量移除。", - "transitImportBtn": "匯出", + "transitImportBtn": "匯入", "transitImportErrorColumnCount": "資料量不足,需要 {columns} 個欄位", "@transitImportErrorColumnCount": { "placeholders": { @@ -1277,13 +1277,39 @@ "menuSubtitle": "產品種類、產品", "menuTutorialTitle": "建立屬於你的菜單", "menuTutorialContent": "首先我們來開始建立一份菜單吧!", + "menuTutorialCreateExample": "幫助建立一份範例菜單以供測試。", "menuSearchHint": "搜尋產品、成分、份量", "menuSearchNotFound": "搜尋不到相關資訊,打錯字了嗎?", + "menuExampleCatalogBurger": "🍔 漢堡", + "menuExampleCatalogDrink": "🍻 飲品", + "menuExampleCatalogSide": "🍰 點心", + "menuExampleCatalogOther": "🛍 其他", + "menuExampleProductCheeseBurger": "起司漢堡", + "menuExampleProductVeggieBurger": "蔬菜漢堡", + "menuExampleProductHamBurger": "火腿漢堡", + "menuExampleProductCola": "可樂", + "menuExampleProductCoffee": "咖啡", + "menuExampleProductFries": "薯條", + "menuExampleProductStraw": "吸管", + "menuExampleProductPlasticBag": "塑膠袋", + "menuExampleIngredientCheese": "🧀 起司", + "menuExampleIngredientLettuce": "🥬 萵苣", + "menuExampleIngredientTomato": "🍅 番茄", + "menuExampleIngredientBun": "🍞 麵包", + "menuExampleIngredientChili": "🌶 醬料", + "menuExampleIngredientHam": "🍖 火腿", + "menuExampleIngredientCola": "🥤 可樂罐", + "menuExampleIngredientCoffee": "☕️ 濾掛咖啡包", + "menuExampleIngredientFries": "🍟 薯條(300g)", + "menuExampleIngredientStraw": "吸管(根)", + "menuExampleIngredientPlasticBag": "塑膠袋(個)", + "menuExampleQuantitySmall": "少量", + "menuExampleQuantityLarge": "增量", + "menuExampleQuantityNone": "無", "menuCatalogHeaderInfo": "種類", "@menuCatalogHeaderInfo": { "description": "Displayed on the upper rectangle in homepage" }, - "menuCatalogTutorialTitle": "Create First Catalog", "menuCatalogEmptyBody": "我們會把相似「產品」放在「產品種類」中,\n到時候點餐會比較方便,例如:\n• 「起司漢堡」、「蔬菜漢堡」整合進「漢堡」\n• 「塑膠袋」、「環保杯」整合進「其他」", "menuCatalogTitleCreate": "新增產品種類", "@menuCatalogTitleCreate": { @@ -1536,6 +1562,8 @@ "cashierSurplusDiffTotalHelper": "和收銀機最一開始的總額的差額。\n這可以快速幫你了解今天收銀機多了多少錢唷。", "orderTitle": "點餐", "orderBtn": "點餐", + "orderTutorialTitle": "開始點餐!", + "orderTutorialContent": "一旦設定好菜單,就可以開始點餐囉\n讓我們趕緊進去看看有什麼吧!\n", "orderSnackbarCashierNotEnough": "收銀機錢不夠找囉!", "orderSnackbarCashierUsingSmallMoney": "收銀機使用小錢去找零", "orderSnackbarCashierUsingSmallMoneyHelper": "找錢給顧客時,收銀機無法使用最適合的錢,就會顯示這個訊息。\n\n例如,售價「65」,消費者支付「100」,此時應找「35」\n如果收銀機只有兩個十元,且有三個以上的五元,就會顯示本訊息。\n\n怎麼避免本提示:\n• 到換錢頁面把各幣值補足。\n• 到[設定頁]({link})關閉收銀機的相關提示。", diff --git a/lib/main.dart b/lib/main.dart index 0699f4a5..22f1e2e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,6 @@ import 'package:possystem/models/analysis/analysis.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:provider/provider.dart'; -import 'debug/setup_menu.dart'; import 'firebase_compatible_options.dart'; import 'helpers/logger.dart'; import 'models/repository/cashier.dart'; @@ -66,10 +65,6 @@ void main() async { // Last for setup ingredient and quantity await Menu().initialize(); - if (isLocalTest) { - await debugSetupMenu(); - } - /// Why use provider? /// https://stackoverflow.com/questions/57157823/provider-vs-inheritedwidget runApp(MultiProvider( diff --git a/lib/models/objects/menu_object.dart b/lib/models/objects/menu_object.dart index 53a6f0f0..77d17b1f 100644 --- a/lib/models/objects/menu_object.dart +++ b/lib/models/objects/menu_object.dart @@ -271,9 +271,9 @@ class ProductQuantityObject extends ModelObject { return ProductQuantityObject( id: id as String, quantityId: quantityId as String, - amount: data['amount'] as num, - additionalCost: data['additionalCost'] as num, - additionalPrice: data['additionalPrice'] as num, + amount: data['amount'] as num? ?? 0, + additionalCost: data['additionalCost'] as num? ?? 0, + additionalPrice: data['additionalPrice'] as num? ?? 0, version: version, ); } diff --git a/lib/ui/home/setting_view.dart b/lib/ui/home/setting_view.dart index 6377008c..3fd428c7 100644 --- a/lib/ui/home/setting_view.dart +++ b/lib/ui/home/setting_view.dart @@ -6,9 +6,11 @@ import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/app_themes.dart'; import 'package:possystem/constants/constant.dart'; import 'package:possystem/debug/debug_page.dart'; +import 'package:possystem/helpers/setup_menu.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/routes.dart'; +import 'package:possystem/services/cache.dart'; import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; @@ -24,6 +26,7 @@ class SettingView extends StatefulWidget { class _SettingViewState extends State with AutomaticKeepAliveClientMixin { late final TutorialInTab? tab; + final GlobalKey<_TutorialCreateExampleMenuListTileState> _tutorialCheckbox = GlobalKey(); @override Widget build(BuildContext context) { @@ -31,92 +34,119 @@ class _SettingViewState extends State with AutomaticKeepAliveClient return TutorialWrapper( tab: tab, - child: ListView(padding: const EdgeInsets.only(bottom: 76), children: [ - const _HeaderInfoList(), - if (!isProd) - ListTile( - key: const Key('setting.debug'), - leading: const Icon(Icons.bug_report_sharp), - title: const Text('Debug'), - subtitle: const Text('For developer only'), - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const DebugPage()), + child: Scaffold( + floatingActionButton: (Cache.instance.get('tutorial.home.order') ?? false) + ? null + : Tutorial( + id: 'home.order', + // 0 if menu tutorial is not shown (only index 0 will trigger tutorial) + index: Menu.instance.isNotEmpty ? 0 : 1, + spotlightBuilder: const SpotlightRectBuilder(borderRadius: 16.0), + title: S.menuTutorialTitle, + message: S.menuTutorialContent, + child: FloatingActionButton.extended( + onPressed: null, + icon: const Icon(Icons.store_sharp), + label: Text(S.orderBtn), + ), + ), + body: ListView(padding: const EdgeInsets.only(bottom: 76), children: [ + const _HeaderInfoList(), + if (!isProd) + ListTile( + key: const Key('setting.debug'), + leading: const Icon(Icons.bug_report_sharp), + title: const Text('Debug'), + subtitle: const Text('For developer only'), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const DebugPage()), + ), + ), + Tutorial( + id: 'home.menu', + index: 0, + title: S.menuTutorialTitle, + message: S.menuTutorialContent, + below: _TutorialCreateExampleMenuListTile(key: _tutorialCheckbox), + spotlightBuilder: const SpotlightRectBuilder(), + disable: Menu.instance.isNotEmpty, + action: () async { + if (_tutorialCheckbox.currentState?.createExampleMenu == true) { + await helpSetupMenu(); + } else { + await context.pushNamed(Routes.menu); + } + }, + child: _buildRouteTile( + id: 'menu', + icon: Icons.collections_sharp, + route: Routes.menu, + title: S.menuTitle, + subtitle: S.menuSubtitle, ), ), - Tutorial( - id: 'home.menu', - title: S.menuTutorialTitle, - message: S.menuTutorialContent, - spotlightBuilder: const SpotlightRectBuilder(), - disable: Menu.instance.isNotEmpty, - route: Routes.menu, - child: _buildRouteTile( - id: 'menu', - icon: Icons.collections_sharp, - route: Routes.menu, - title: S.menuTitle, - subtitle: S.menuSubtitle, + Tutorial( + id: 'home.exporter', + index: 3, + title: S.transitTutorialTitle, + message: S.transitTutorialContent, + spotlightBuilder: const SpotlightRectBuilder(), + child: _buildRouteTile( + id: 'exporter', + icon: Icons.upload_file_sharp, + route: Routes.transit, + title: S.transitTitle, + subtitle: S.transitDescription, + ), ), - ), - Tutorial( - id: 'home.exporter', - title: S.transitTutorialTitle, - message: S.transitTutorialContent, - spotlightBuilder: const SpotlightRectBuilder(), - child: _buildRouteTile( - id: 'exporter', - icon: Icons.upload_file_sharp, - route: Routes.transit, - title: S.transitTitle, - subtitle: S.transitDescription, + Tutorial( + id: 'home.order_attr', + index: 2, + title: S.orderAttributeTutorialTitle, + message: S.orderAttributeTutorialContent, + spotlightBuilder: const SpotlightRectBuilder(), + child: _buildRouteTile( + id: 'order_attrs', + icon: Icons.assignment_ind_sharp, + route: Routes.orderAttr, + title: S.orderAttributeTitle, + subtitle: S.orderAttributeDescription, + ), ), - ), - Tutorial( - id: 'home.order_attr', - title: S.orderAttributeTutorialTitle, - message: S.orderAttributeTutorialContent, - spotlightBuilder: const SpotlightRectBuilder(), - child: _buildRouteTile( - id: 'order_attrs', - icon: Icons.assignment_ind_sharp, - route: Routes.orderAttr, - title: S.orderAttributeTitle, - subtitle: S.orderAttributeDescription, + _buildRouteTile( + id: 'quantity', + icon: Icons.exposure_sharp, + route: Routes.quantity, + title: S.stockQuantityTitle, + subtitle: S.stockQuantityDescription, ), - ), - _buildRouteTile( - id: 'quantity', - icon: Icons.exposure_sharp, - route: Routes.quantity, - title: S.stockQuantityTitle, - subtitle: S.stockQuantityDescription, - ), - _buildRouteTile( - id: 'feature_request', - icon: Icons.lightbulb_sharp, - route: Routes.featureRequest, - title: S.settingElfTitle, - subtitle: S.settingElfDescription, - ), - _buildRouteTile( - id: 'setting', - icon: Icons.settings_sharp, - route: Routes.features, - title: S.settingFeatureTitle, - subtitle: S.settingFeatureDescription, - ), - Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - TextButton( - onPressed: _bottomLinks[0].launch, - child: Text(_bottomLinks[0].text), + _buildRouteTile( + id: 'feature_request', + icon: Icons.lightbulb_sharp, + route: Routes.featureRequest, + title: S.settingElfTitle, + subtitle: S.settingElfDescription, ), - const Text(MetaBlock.string), - TextButton( - onPressed: _bottomLinks[1].launch, - child: Text(_bottomLinks[1].text), + _buildRouteTile( + id: 'setting', + icon: Icons.settings_sharp, + route: Routes.features, + title: S.settingFeatureTitle, + subtitle: S.settingFeatureDescription, ), - ]) - ]), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + TextButton( + onPressed: _bottomLinks[0].launch, + child: Text(_bottomLinks[0].text), + ), + const Text(MetaBlock.string), + TextButton( + onPressed: _bottomLinks[1].launch, + child: Text(_bottomLinks[1].text), + ), + ]) + ]), + ), ); } @@ -236,6 +266,26 @@ class _HeaderInfoList extends StatelessWidget { } } +class _TutorialCreateExampleMenuListTile extends StatefulWidget { + const _TutorialCreateExampleMenuListTile({super.key}); + + @override + State<_TutorialCreateExampleMenuListTile> createState() => _TutorialCreateExampleMenuListTileState(); +} + +class _TutorialCreateExampleMenuListTileState extends State<_TutorialCreateExampleMenuListTile> { + bool createExampleMenu = true; + + @override + Widget build(BuildContext context) { + return CheckboxListTile( + value: createExampleMenu, + onChanged: (v) => setState(() => createExampleMenu = v!), + title: Text(S.menuTutorialCreateExample), + ); + } +} + const _bottomLinks = [ LinkifyData('Privacy Policy', 'https://evan361425.github.io/flutter-pos-system/PRIVACY_POLICY/'), LinkifyData('License', 'https://evan361425.github.io/flutter-pos-system/LICENSE/'), diff --git a/lib/ui/menu/menu_page.dart b/lib/ui/menu/menu_page.dart index e31ee384..5a91743a 100644 --- a/lib/ui/menu/menu_page.dart +++ b/lib/ui/menu/menu_page.dart @@ -76,7 +76,14 @@ class _MenuPageState extends State { ), ], ), - floatingActionButton: widget.productOnly ? null : fab, + floatingActionButton: widget.productOnly + ? null + : FloatingActionButton( + key: const Key('menu.add'), + onPressed: _handleCreate, + tooltip: selected == null ? S.menuCatalogTitleCreate : S.menuProductTitleCreate, + child: const Icon(KIcons.add), + ), body: PageView( controller: controller, // disable scrolling, only control by program @@ -113,22 +120,6 @@ class _MenuPageState extends State { super.dispose(); } - Widget get fab { - return Tutorial( - id: 'add_menu', - disable: Menu.instance.isNotEmpty, - title: S.menuCatalogTutorialTitle, - message: S.menuCatalogEmptyBody, - route: Routes.menuNew, - child: FloatingActionButton( - key: const Key('menu.add'), - onPressed: _handleCreate, - tooltip: selected == null ? S.menuCatalogTitleCreate : S.menuProductTitleCreate, - child: const Icon(KIcons.add), - ), - ); - } - Widget get firstView { if (Menu.instance.isEmpty) { return Center( diff --git a/pubspec.lock b/pubspec.lock index ad05c57d..8e7c047c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1110,10 +1110,10 @@ packages: dependency: "direct main" description: name: spotlight_ant - sha256: a295d3dea0f54bc8bb82042c166e1ca0bedb9c97353ec3f1d67bc3ed5b6fd73e + sha256: "30c1fb6dbb2356dc92e61da332bd5cf7bf63e4b74c1414c618caab34127ed96a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" sprintf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 171a618f..1c49f334 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: # components table_calendar: ^3.1.1 # 24, 02-09 syncfusion_flutter_charts: ^25.2.4 - spotlight_ant: ^1.1.0 + spotlight_ant: ^1.1.1 # image image: ^4.1.7 # 24, 01-10 diff --git a/test/debug/setup_menu_test.dart b/test/helpers/setup_menu_test.dart similarity index 80% rename from test/debug/setup_menu_test.dart rename to test/helpers/setup_menu_test.dart index fc8dc9b3..f17d2f2d 100644 --- a/test/debug/setup_menu_test.dart +++ b/test/helpers/setup_menu_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:possystem/debug/setup_menu.dart'; +import 'package:possystem/helpers/setup_menu.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stock.dart'; @@ -8,14 +8,14 @@ import 'package:possystem/models/repository/stock.dart'; import '../mocks/mock_storage.dart'; void main() { - group('Setup Menu in DEBUG mode', () { + group('Setup Menu', () { test('Should add once', () async { when(storage.add(any, any, any)).thenAnswer((_) => Future.value()); - await debugSetupMenu(); + await helpSetupMenu(); verify(storage.add(any, any, any)); - await debugSetupMenu(); + await helpSetupMenu(); verifyNever(storage.add(any, any, any)); }); From 6c302add5d2a9b9b58bf4ae5cefed0de146831c3 Mon Sep 17 00:00:00 2001 From: Lu Shueh Chou Date: Mon, 24 Jun 2024 07:59:34 +0800 Subject: [PATCH 3/5] fix test --- .../{setup_menu.dart => setup_example.dart} | 2 +- lib/ui/home/setting_view.dart | 4 ++-- release.config.json | 20 ++++++++----------- ...menu_test.dart => setup_example_test.dart} | 8 +++++--- 4 files changed, 16 insertions(+), 18 deletions(-) rename lib/helpers/{setup_menu.dart => setup_example.dart} (99%) rename test/helpers/{setup_menu_test.dart => setup_example_test.dart} (77%) diff --git a/lib/helpers/setup_menu.dart b/lib/helpers/setup_example.dart similarity index 99% rename from lib/helpers/setup_menu.dart rename to lib/helpers/setup_example.dart index 3719d67a..aa87786c 100644 --- a/lib/helpers/setup_menu.dart +++ b/lib/helpers/setup_example.dart @@ -9,7 +9,7 @@ import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/quantity.dart'; import 'package:possystem/translator.dart'; -Future helpSetupMenu() async { +Future setupExampleMenu() async { if (Menu.instance.isNotEmpty) return; log('setting stock', name: 'example menu'); diff --git a/lib/ui/home/setting_view.dart b/lib/ui/home/setting_view.dart index 3fd428c7..9872a4a3 100644 --- a/lib/ui/home/setting_view.dart +++ b/lib/ui/home/setting_view.dart @@ -6,7 +6,7 @@ import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/app_themes.dart'; import 'package:possystem/constants/constant.dart'; import 'package:possystem/debug/debug_page.dart'; -import 'package:possystem/helpers/setup_menu.dart'; +import 'package:possystem/helpers/setup_example.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/routes.dart'; @@ -72,7 +72,7 @@ class _SettingViewState extends State with AutomaticKeepAliveClient disable: Menu.instance.isNotEmpty, action: () async { if (_tutorialCheckbox.currentState?.createExampleMenu == true) { - await helpSetupMenu(); + await setupExampleMenu(); } else { await context.pushNamed(Routes.menu); } diff --git a/release.config.json b/release.config.json index 9fac8f8a..581d53f6 100644 --- a/release.config.json +++ b/release.config.json @@ -1,29 +1,25 @@ { "categories": [ { - "title": "## 🚀 功能", + "title": "## 🚀 Features", "labels": ["enhancement"] }, { - "title": "## 🐛 修正", + "title": "## 🐛 Bugs", "labels": ["bug"] } ], - "ignore_labels": [ - "ignore" - ], + "ignore_labels": ["ignore"], "sort": "ASC", - "template": "${{CHANGELOG}}\n\n## 👻 其他\n\n${{UNCATEGORIZED}}\n", + "template": "${{CHANGELOG}}\n\n## 👻 Others\n\n${{UNCATEGORIZED}}\n", "pr_template": "- ${{TITLE}} #${{NUMBER}} : ${{AUTHOR}}", - "empty_template": "沒找到相關的 PR,可能需要查找 DIFF", - "max_tags_to_fetch": 200, - "max_pull_requests": 200, + "empty_template": "Caught some bugs, time to clean up!", + "max_tags_to_fetch": 10, + "max_pull_requests": 20, "max_back_track_time_days": 365, "exclude_merge_branches": [], "tag_resolver": { "method": "semver" }, - "base_branches": [ - "master" - ] + "base_branches": ["master"] } diff --git a/test/helpers/setup_menu_test.dart b/test/helpers/setup_example_test.dart similarity index 77% rename from test/helpers/setup_menu_test.dart rename to test/helpers/setup_example_test.dart index f17d2f2d..afd1770d 100644 --- a/test/helpers/setup_menu_test.dart +++ b/test/helpers/setup_example_test.dart @@ -1,26 +1,28 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:possystem/helpers/setup_menu.dart'; +import 'package:possystem/helpers/setup_example.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stock.dart'; import '../mocks/mock_storage.dart'; +import '../test_helpers/translator.dart'; void main() { group('Setup Menu', () { test('Should add once', () async { when(storage.add(any, any, any)).thenAnswer((_) => Future.value()); - await helpSetupMenu(); + await setupExampleMenu(); verify(storage.add(any, any, any)); - await helpSetupMenu(); + await setupExampleMenu(); verifyNever(storage.add(any, any, any)); }); setUpAll(() { initializeStorage(); + initializeTranslator(); Menu(); Stock(); Quantities(); From a7c146c06c1b0e8b1b1aac825c98f542c2c0f7ef Mon Sep 17 00:00:00 2001 From: Lu Shueh Chou Date: Sat, 29 Jun 2024 09:41:08 +0800 Subject: [PATCH 4/5] set up example menu --- assets/l10n/en/menu.yaml | 37 +++++++------- assets/l10n/zh/menu.yaml | 26 +++++----- lib/components/tutorial.dart | 43 ++++------------ lib/helpers/setup_example.dart | 26 +++++----- lib/l10n/app_en.arb | 32 ++++++------ lib/l10n/app_zh.arb | 28 +++++----- lib/models/repository.dart | 2 +- lib/ui/home/setting_view.dart | 21 ++++---- test/components/tutorial_test.dart | 4 +- test/ui/home/home_page_test.dart | 82 ++++++++++++++++++++++++++++++ 10 files changed, 180 insertions(+), 121 deletions(-) diff --git a/assets/l10n/en/menu.yaml b/assets/l10n/en/menu.yaml index 9b5afc1d..4e8042e2 100644 --- a/assets/l10n/en/menu.yaml +++ b/assets/l10n/en/menu.yaml @@ -10,10 +10,10 @@ search: notFound: Couldn't find relevant information. Did you misspell something? example: catalog: - burger: '🍔 Burgers' - drink: '🍻 Drinks' - side: '🍰 Side' - other: '🛍 Others' + burger: Burgers + drink: Drinks + side: Side + other: Others product: cheeseBurger: Cheese Burger veggieBurger: Veggie Burger @@ -24,17 +24,17 @@ example: straw: Straw plasticBag: Plastic Bag ingredient: - cheese: '🧀 Cheese' - lettuce: '🥬 Lettuce' - tomato: '🍅 Tomato' - bun: '🍞 Bun' - chili: '🌶 Chili Sauce' - ham: '🍖 Ham' - cola: '🥤 Can of Cola' - coffee: '☕️ Drip Coffee' - fries: '🍟 Bag of Fries' - straw: 'Straw' - plasticBag: 'Plastic Bag' + cheese: Cheese + lettuce: Lettuce + tomato: Tomato + bun: Bun + chili: Chili Sauce + ham: Ham + cola: Can of Cola + coffee: Drip Coffee + fries: Bag of Fries + straw: Straw + plasticBag: Plastic Bag quantity: small: Small large: Large @@ -44,8 +44,8 @@ catalog: - Categories - Displayed on the upper rectangle in homepage emptyBody: |- - Similar "products" will be grouped under "categories", - making it convenient for ordering, such as: + Similar products will be grouped under categories, + making ordering convenient, such as: • "Cheese Burger", "Veggie Burger" > "Burgers" • "Plastic Bag", "Eco Cup" > "Others" title: @@ -56,7 +56,8 @@ catalog: reorder: Reorder Categories dialogDeletionContent: - =0: No products inside - other: Will delete {count} products together + =1: Will also delete {count} related product + other: Will also delete {count} related products - Warning message when deleting product categories on the menu page - count: {type: int, mode: plural} name: diff --git a/assets/l10n/zh/menu.yaml b/assets/l10n/zh/menu.yaml index bcecf2bc..6de91419 100644 --- a/assets/l10n/zh/menu.yaml +++ b/assets/l10n/zh/menu.yaml @@ -10,10 +10,10 @@ search: notFound: 搜尋不到相關資訊,打錯字了嗎? example: catalog: - burger: '🍔 漢堡' - drink: '🍻 飲品' - side: '🍰 點心' - other: '🛍 其他' + burger: 漢堡 + drink: 飲品 + side: 點心 + other: 其他 product: cheeseBurger: 起司漢堡 veggieBurger: 蔬菜漢堡 @@ -24,15 +24,15 @@ example: straw: 吸管 plasticBag: 塑膠袋 ingredient: - cheese: '🧀 起司' - lettuce: '🥬 萵苣' - tomato: '🍅 番茄' - bun: '🍞 麵包' - chili: '🌶 醬料' - ham: '🍖 火腿' - cola: '🥤 可樂罐' - coffee: '☕️ 濾掛咖啡包' - fries: '🍟 薯條(300g)' + cheese: 起司 + lettuce: 萵苣 + tomato: 番茄 + bun: 麵包 + chili: 醬料 + ham: 火腿 + cola: 可樂罐 + coffee: 濾掛咖啡包 + fries: 薯條(300g) straw: 吸管(根) plasticBag: 塑膠袋(個) quantity: diff --git a/lib/components/tutorial.dart b/lib/components/tutorial.dart index 21d09a9e..8c6fce1d 100644 --- a/lib/components/tutorial.dart +++ b/lib/components/tutorial.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:possystem/services/cache.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; @@ -56,17 +55,10 @@ class Tutorial extends StatelessWidget { final Widget child; - final SpotlightDurationConfig duration; - - /// route to be pushed after the tutorial is dismissed - /// - /// if [action] is provided, this will be ignored - final String? route; - /// action to be executed after the tutorial is dismissed final Future Function()? action; - final bool _hasAction; + static bool debug = false; const Tutorial({ super.key, @@ -79,16 +71,9 @@ class Tutorial extends StatelessWidget { this.padding = const EdgeInsets.all(8), this.disable = false, this.monitorVisibility = false, - this.route, this.action, required this.child, - this.duration = const SpotlightDurationConfig( - bump: Duration(milliseconds: 500), - zoomIn: Duration(milliseconds: 600), - zoomOut: Duration(milliseconds: 600), - contentFadeIn: Duration(milliseconds: 200), - ), - }) : _hasAction = route != null || action != null; + }); @override Widget build(BuildContext context) { @@ -96,37 +81,27 @@ class Tutorial extends StatelessWidget { return child; } - final goSkip = _hasAction ? () => SpotlightAntAction.skip : null; + final theme = Theme.of(context); + SpotlightAnt.debug = true; return SpotlightAnt( enable: enabled, index: index, - duration: duration, + duration: debug ? SpotlightDurationConfig.zero : const SpotlightDurationConfig(), monitorId: monitorVisibility ? 'tutorial.$id' : null, onDismiss: _onDismiss, - onDismissed: _hasAction - ? () async { - await (action?.call() ?? context.pushNamed(route!)); - - // try start the next tutorial, if exists - if (context.mounted) { - SpotlightShow.maybeOf(context)?.start(); - } - } - : null, + onDismissed: action, spotlight: SpotlightConfig( builder: spotlightBuilder, padding: padding, - onTap: goSkip, ), - backdrop: SpotlightBackdropConfig(onTap: goSkip), + backdrop: const SpotlightBackdropConfig(), action: const SpotlightActionConfig( enabled: [SpotlightAntAction.prev, SpotlightAntAction.next], ), content: SpotlightContent( + fontSize: theme.textTheme.titleMedium!.fontSize, child: Column(children: [ - if (title != null) - // headline medium style - Text(title!, style: const TextStyle(fontSize: 34, fontWeight: FontWeight.bold, wordSpacing: 0.25)), + if (title != null) Text(title!, style: theme.textTheme.headlineMedium!.copyWith(color: Colors.white)), const SizedBox(height: 16), Text(message), if (below != null) below!, diff --git a/lib/helpers/setup_example.dart b/lib/helpers/setup_example.dart index aa87786c..f2c00c84 100644 --- a/lib/helpers/setup_example.dart +++ b/lib/helpers/setup_example.dart @@ -14,15 +14,15 @@ Future setupExampleMenu() async { log('setting stock', name: 'example menu'); for (final e in [ - Ingredient(id: 'cheese', name: S.menuExampleIngredientCheese, currentAmount: 30, totalAmount: 30), - Ingredient(id: 'lettuce', name: S.menuExampleIngredientLettuce, currentAmount: 70, totalAmount: 70), - Ingredient(id: 'tomato', name: S.menuExampleIngredientTomato, currentAmount: 100, totalAmount: 100), - Ingredient(id: 'bun', name: S.menuExampleIngredientBun, currentAmount: 50, totalAmount: 50), - Ingredient(id: 'chili', name: S.menuExampleIngredientChili, currentAmount: 500, totalAmount: 500), - Ingredient(id: 'ham', name: S.menuExampleIngredientHam, currentAmount: 5, totalAmount: 5), - Ingredient(id: 'cola', name: S.menuExampleIngredientCola, currentAmount: 20, totalAmount: 20), - Ingredient(id: 'coffee', name: S.menuExampleIngredientCoffee, currentAmount: 50, totalAmount: 50), - Ingredient(id: 'fries', name: S.menuExampleIngredientFries, currentAmount: 3, totalAmount: 3), + Ingredient(id: 'cheese', name: '🧀 ${S.menuExampleIngredientCheese}', currentAmount: 30, totalAmount: 30), + Ingredient(id: 'lettuce', name: '🥬 ${S.menuExampleIngredientLettuce}', currentAmount: 70, totalAmount: 70), + Ingredient(id: 'tomato', name: '🍅 ${S.menuExampleIngredientTomato}', currentAmount: 100, totalAmount: 100), + Ingredient(id: 'bun', name: '🍞 ${S.menuExampleIngredientBun}', currentAmount: 50, totalAmount: 50), + Ingredient(id: 'chili', name: '🌶 ${S.menuExampleIngredientChili}', currentAmount: 500, totalAmount: 500), + Ingredient(id: 'ham', name: '🍖 ${S.menuExampleIngredientHam}', currentAmount: 5, totalAmount: 5), + Ingredient(id: 'cola', name: '🥤 ${S.menuExampleIngredientCola}', currentAmount: 20, totalAmount: 20), + Ingredient(id: 'coffee', name: '☕️ ${S.menuExampleIngredientCoffee}', currentAmount: 50, totalAmount: 50), + Ingredient(id: 'fries', name: '🍟 ${S.menuExampleIngredientFries}', currentAmount: 3, totalAmount: 3), Ingredient(id: 'straw', name: S.menuExampleIngredientStraw, currentAmount: 50, totalAmount: 50), Ingredient(id: 'plasticBag', name: S.menuExampleIngredientPlasticBag, currentAmount: 50, totalAmount: 50), ]) { @@ -44,7 +44,7 @@ Future setupExampleMenu() async { Catalog.fromObject(CatalogObject.build({ "id": "burger", "index": 1, - "name": S.menuExampleCatalogBurger, + "name": '🍔 ${S.menuExampleCatalogBurger}', "createdAt": now, "products": { "cheese-burger": { @@ -126,7 +126,7 @@ Future setupExampleMenu() async { Catalog.fromObject(CatalogObject.build({ "id": "drink", "index": 2, - "name": S.menuExampleCatalogDrink, + "name": '🍻 ${S.menuExampleCatalogDrink}', "createdAt": now, "products": { "cola": { @@ -155,7 +155,7 @@ Future setupExampleMenu() async { Catalog.fromObject(CatalogObject.build({ "id": "side", "index": 3, - "name": S.menuExampleCatalogSide, + "name": '🍰 ${S.menuExampleCatalogSide}', "createdAt": now, "products": { "fries": { @@ -179,7 +179,7 @@ Future setupExampleMenu() async { Catalog.fromObject(CatalogObject.build({ "id": "other", "index": 4, - "name": S.menuExampleCatalogOther, + "name": '🛍 ${S.menuExampleCatalogOther}', "createdAt": now, "products": { "plastic-bag": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 238111a9..97c5b611 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2024-06-22T02:38:59.210340Z", + "@@last_modified": "2024-06-27T05:21:06.324318Z", "@@author": "Lu Shueh Chou", "settingTab": "Settings", "settingVersion": "Version: {version}", @@ -1280,10 +1280,10 @@ "menuTutorialCreateExample": "Help create an example menu to test.", "menuSearchHint": "Search for products, ingredients, quantities", "menuSearchNotFound": "Couldn't find relevant information. Did you misspell something?", - "menuExampleCatalogBurger": "🍔 Burgers", - "menuExampleCatalogDrink": "🍻 Drinks", - "menuExampleCatalogSide": "🍰 Side", - "menuExampleCatalogOther": "🛍 Others", + "menuExampleCatalogBurger": "Burgers", + "menuExampleCatalogDrink": "Drinks", + "menuExampleCatalogSide": "Side", + "menuExampleCatalogOther": "Others", "menuExampleProductCheeseBurger": "Cheese Burger", "menuExampleProductVeggieBurger": "Veggie Burger", "menuExampleProductHamBurger": "Ham Burger", @@ -1292,15 +1292,15 @@ "menuExampleProductFries": "Fries", "menuExampleProductStraw": "Straw", "menuExampleProductPlasticBag": "Plastic Bag", - "menuExampleIngredientCheese": "🧀 Cheese", - "menuExampleIngredientLettuce": "🥬 Lettuce", - "menuExampleIngredientTomato": "🍅 Tomato", - "menuExampleIngredientBun": "🍞 Bun", - "menuExampleIngredientChili": "🌶 Chili Sauce", - "menuExampleIngredientHam": "🍖 Ham", - "menuExampleIngredientCola": "🥤 Can of Cola", - "menuExampleIngredientCoffee": "☕️ Drip Coffee", - "menuExampleIngredientFries": "🍟 Bag of Fries", + "menuExampleIngredientCheese": "Cheese", + "menuExampleIngredientLettuce": "Lettuce", + "menuExampleIngredientTomato": "Tomato", + "menuExampleIngredientBun": "Bun", + "menuExampleIngredientChili": "Chili Sauce", + "menuExampleIngredientHam": "Ham", + "menuExampleIngredientCola": "Can of Cola", + "menuExampleIngredientCoffee": "Drip Coffee", + "menuExampleIngredientFries": "Bag of Fries", "menuExampleIngredientStraw": "Straw", "menuExampleIngredientPlasticBag": "Plastic Bag", "menuExampleQuantitySmall": "Small", @@ -1310,14 +1310,14 @@ "@menuCatalogHeaderInfo": { "description": "Displayed on the upper rectangle in homepage" }, - "menuCatalogEmptyBody": "Similar \"products\" will be grouped under \"categories\",\nmaking it convenient for ordering, such as:\n• \"Cheese Burger\", \"Veggie Burger\" > \"Burgers\"\n• \"Plastic Bag\", \"Eco Cup\" > \"Others\"", + "menuCatalogEmptyBody": "Similar products will be grouped under categories,\nmaking ordering convenient, such as:\n• \"Cheese Burger\", \"Veggie Burger\" > \"Burgers\"\n• \"Plastic Bag\", \"Eco Cup\" > \"Others\"", "menuCatalogTitleCreate": "Add Category", "@menuCatalogTitleCreate": { "description": "FloatingActionButton description on the menu page" }, "menuCatalogTitleUpdate": "Edit Category", "menuCatalogTitleReorder": "Reorder Categories", - "menuCatalogDialogDeletionContent": "{count, plural, =0{No products inside} other{Will delete {count} products together}}", + "menuCatalogDialogDeletionContent": "{count, plural, =0{No products inside} =1{Will also delete {count} related product} other{Will also delete {count} related products}}", "@menuCatalogDialogDeletionContent": { "description": "Warning message when deleting product categories on the menu page", "placeholders": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6da8ae50..b2fc7da6 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "@@last_modified": "2024-06-22T02:38:59.251648Z", + "@@last_modified": "2024-06-27T05:21:06.347696Z", "@@author": "Lu Shueh Chou", "settingTab": "設定", "settingVersion": "版本:{version}", @@ -1280,10 +1280,10 @@ "menuTutorialCreateExample": "幫助建立一份範例菜單以供測試。", "menuSearchHint": "搜尋產品、成分、份量", "menuSearchNotFound": "搜尋不到相關資訊,打錯字了嗎?", - "menuExampleCatalogBurger": "🍔 漢堡", - "menuExampleCatalogDrink": "🍻 飲品", - "menuExampleCatalogSide": "🍰 點心", - "menuExampleCatalogOther": "🛍 其他", + "menuExampleCatalogBurger": "漢堡", + "menuExampleCatalogDrink": "飲品", + "menuExampleCatalogSide": "點心", + "menuExampleCatalogOther": "其他", "menuExampleProductCheeseBurger": "起司漢堡", "menuExampleProductVeggieBurger": "蔬菜漢堡", "menuExampleProductHamBurger": "火腿漢堡", @@ -1292,15 +1292,15 @@ "menuExampleProductFries": "薯條", "menuExampleProductStraw": "吸管", "menuExampleProductPlasticBag": "塑膠袋", - "menuExampleIngredientCheese": "🧀 起司", - "menuExampleIngredientLettuce": "🥬 萵苣", - "menuExampleIngredientTomato": "🍅 番茄", - "menuExampleIngredientBun": "🍞 麵包", - "menuExampleIngredientChili": "🌶 醬料", - "menuExampleIngredientHam": "🍖 火腿", - "menuExampleIngredientCola": "🥤 可樂罐", - "menuExampleIngredientCoffee": "☕️ 濾掛咖啡包", - "menuExampleIngredientFries": "🍟 薯條(300g)", + "menuExampleIngredientCheese": "起司", + "menuExampleIngredientLettuce": "萵苣", + "menuExampleIngredientTomato": "番茄", + "menuExampleIngredientBun": "麵包", + "menuExampleIngredientChili": "醬料", + "menuExampleIngredientHam": "火腿", + "menuExampleIngredientCola": "可樂罐", + "menuExampleIngredientCoffee": "濾掛咖啡包", + "menuExampleIngredientFries": "薯條(300g)", "menuExampleIngredientStraw": "吸管(根)", "menuExampleIngredientPlasticBag": "塑膠袋(個)", "menuExampleQuantitySmall": "少量", diff --git a/lib/models/repository.dart b/lib/models/repository.dart index 83346f28..8176101d 100644 --- a/lib/models/repository.dart +++ b/lib/models/repository.dart @@ -224,7 +224,7 @@ mixin RepositoryStorage on Repository { @override Future saveItem(T item) { - Log.ger('add start', storageStore.name, _items.toString()); + Log.ger('add start', storageStore.name, item.toString()); final data = item.toObject().toMap(); return repoType == RepositoryStorageType.pureRepo diff --git a/lib/ui/home/setting_view.dart b/lib/ui/home/setting_view.dart index 9872a4a3..ff18c7a0 100644 --- a/lib/ui/home/setting_view.dart +++ b/lib/ui/home/setting_view.dart @@ -42,9 +42,10 @@ class _SettingViewState extends State with AutomaticKeepAliveClient // 0 if menu tutorial is not shown (only index 0 will trigger tutorial) index: Menu.instance.isNotEmpty ? 0 : 1, spotlightBuilder: const SpotlightRectBuilder(borderRadius: 16.0), - title: S.menuTutorialTitle, - message: S.menuTutorialContent, + title: S.orderTutorialTitle, + message: S.orderTutorialContent, child: FloatingActionButton.extended( + heroTag: 'order.tutorial', onPressed: null, icon: const Icon(Icons.store_sharp), label: Text(S.orderBtn), @@ -73,8 +74,6 @@ class _SettingViewState extends State with AutomaticKeepAliveClient action: () async { if (_tutorialCheckbox.currentState?.createExampleMenu == true) { await setupExampleMenu(); - } else { - await context.pushNamed(Routes.menu); } }, child: _buildRouteTile( @@ -259,7 +258,7 @@ class _HeaderInfoList extends StatelessWidget { ), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title.toString(), style: theme.textTheme.headlineMedium), - Text(subtitle, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center), + Flexible(child: Text(subtitle, textAlign: TextAlign.center)), ]), ), ); @@ -278,10 +277,14 @@ class _TutorialCreateExampleMenuListTileState extends State<_TutorialCreateExamp @override Widget build(BuildContext context) { - return CheckboxListTile( - value: createExampleMenu, - onChanged: (v) => setState(() => createExampleMenu = v!), - title: Text(S.menuTutorialCreateExample), + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: CheckboxListTile( + value: createExampleMenu, + onChanged: (v) => setState(() => createExampleMenu = v!), + tileColor: Theme.of(context).primaryColor, + title: Text(S.menuTutorialCreateExample, style: const TextStyle(color: Colors.white)), + ), ); } } diff --git a/test/components/tutorial_test.dart b/test/components/tutorial_test.dart index 153fade8..44a503ec 100644 --- a/test/components/tutorial_test.dart +++ b/test/components/tutorial_test.dart @@ -18,7 +18,6 @@ void main() { id: '1', title: 'title1', message: 'message1', - duration: SpotlightDurationConfig.zero, child: Text('1'), ), ], @@ -78,6 +77,7 @@ void main() { }); setUpAll(() { + Tutorial.debug = true; initializeCache(); }); } @@ -111,7 +111,6 @@ class _ScaffoldState extends State<_Scaffold> with TickerProviderStateMixin { id: '1', title: 'title1', message: 'message1', - duration: SpotlightDurationConfig.zero, child: Text('1'), ), ), @@ -121,7 +120,6 @@ class _ScaffoldState extends State<_Scaffold> with TickerProviderStateMixin { id: '2', title: 'title2', message: 'message2', - duration: SpotlightDurationConfig.zero, child: Text('2'), ), ), diff --git a/test/ui/home/home_page_test.dart b/test/ui/home/home_page_test.dart index 82039445..0df3b3f5 100644 --- a/test/ui/home/home_page_test.dart +++ b/test/ui/home/home_page_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; +import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/app_themes.dart'; import 'package:possystem/models/analysis/analysis.dart'; import 'package:possystem/models/repository/cart.dart'; @@ -16,6 +17,7 @@ import 'package:possystem/my_app.dart'; import 'package:possystem/routes.dart'; import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/settings/settings_provider.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/home/home_page.dart'; import 'package:provider/provider.dart'; @@ -112,6 +114,85 @@ void main() { await navAndCheck('home.analysis', 'anal.history'); }); + testWidgets('Setup example menu', (tester) async { + when(cache.get(any)).thenReturn(null); + when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); + + await tester.pumpWidget(MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: SettingsProvider.instance), + ChangeNotifierProvider.value(value: Menu()), + ChangeNotifierProvider.value(value: Stock()), + ChangeNotifierProvider.value(value: Quantities()), + ChangeNotifierProvider.value(value: OrderAttributes()), + ], + child: MaterialApp.router( + routerConfig: GoRouter(observers: [ + MyApp.routeObserver + ], routes: [ + GoRoute( + path: '/', + routes: Routes.routes, + builder: (_, __) => const HomePage(tab: HomeTab.setting), + ) + ]), + theme: AppThemes.lightTheme, + darkTheme: AppThemes.darkTheme, + ), + )); + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 5)); + + // set up example menu + await tester.tapAt(Offset.zero); + await tester.pump(const Duration(milliseconds: 5)); + await tester.pump(const Duration(milliseconds: 5)); + + expect(find.text(S.orderTutorialTitle), findsOneWidget); + expect(Menu.instance.isNotEmpty, isTrue); + verify(cache.set('tutorial.home.menu', true)); + }); + + testWidgets('Disable example menu', (tester) async { + when(cache.get(any)).thenReturn(null); + when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); + + await tester.pumpWidget(MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: SettingsProvider.instance), + ChangeNotifierProvider.value(value: Menu()), + ChangeNotifierProvider.value(value: Stock()), + ChangeNotifierProvider.value(value: Quantities()), + ChangeNotifierProvider.value(value: OrderAttributes()), + ], + child: MaterialApp.router( + routerConfig: GoRouter(observers: [ + MyApp.routeObserver + ], routes: [ + GoRoute( + path: '/', + routes: Routes.routes, + builder: (_, __) => const HomePage(tab: HomeTab.setting), + ) + ]), + theme: AppThemes.lightTheme, + darkTheme: AppThemes.darkTheme, + ), + )); + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 5)); + + // set up example menu + await tester.tap(find.text(S.menuTutorialCreateExample)); + await tester.tapAt(Offset.zero); + await tester.pump(const Duration(milliseconds: 5)); + await tester.pump(const Duration(milliseconds: 5)); + + expect(find.text(S.orderTutorialTitle), findsOneWidget); + expect(Menu.instance.isNotEmpty, isFalse); + verify(cache.set('tutorial.home.menu', true)); + }); + setUp(() { // setup currency when(cache.get('currency')).thenReturn(null); @@ -129,6 +210,7 @@ void main() { }); setUpAll(() { + Tutorial.debug = true; initializeAuth(); initializeCache(); initializeStorage(); From 84da75f726b58817d169bf031b9ab656cd3bf37a Mon Sep 17 00:00:00 2001 From: Lu Shueh Chou Date: Sat, 6 Jul 2024 20:59:31 +0800 Subject: [PATCH 5/5] add order attributes tutorial --- assets/l10n/en/order_attribute.yaml | 18 ++++ assets/l10n/en/transit.yaml | 6 -- assets/l10n/zh/order_attribute.yaml | 18 ++++ assets/l10n/zh/transit.yaml | 6 -- lib/helpers/setup_example.dart | 50 ++++++++++ lib/l10n/app_en.arb | 15 ++- lib/l10n/app_zh.arb | 15 ++- lib/ui/home/setting_view.dart | 63 ++++++------ test/helpers/setup_example_test.dart | 4 + test/ui/home/home_page_test.dart | 142 +++++++++++++-------------- 10 files changed, 216 insertions(+), 121 deletions(-) diff --git a/assets/l10n/en/order_attribute.yaml b/assets/l10n/en/order_attribute.yaml index e5120f47..1d99581c 100644 --- a/assets/l10n/en/order_attribute.yaml +++ b/assets/l10n/en/order_attribute.yaml @@ -17,6 +17,24 @@ tutorial: content: |- This is where you set customer information, such as dine-in, takeout, office worker, etc. This information helps us track who comes to consume and make better business strategies. + createExample: Help create an example to test. +example: + age: Age + _age: + $prefix: age + child: Child + adult: Adult + senior: Senior + place: Place + _place: + $prefix: place + takeout: Takeout + dineIn: Dine-in + ecoFriendly: Eco-Friendly + _ecoFriendly: + $prefix: ecoFriendly + reusableBottle: Reusable Bottle + reusableBag: Reusable Bag meta: mode: - 'Mode: {name}' diff --git a/assets/l10n/en/transit.yaml b/assets/l10n/en/transit.yaml index 07d68766..5feb5c9c 100644 --- a/assets/l10n/en/transit.yaml +++ b/assets/l10n/en/transit.yaml @@ -1,12 +1,6 @@ $prefix: transit title: Data Transfer description: Importing and Exporting Store Information and Orders -tutorial: - title: Sync Multiple Devices - content: |- - This is where you can import/export menu, inventory, order records, and other information. - - We provide two methods: Google Sheets and plain text, making it convenient to sync data across different devices. method: title: Please Select Transfer Method name: diff --git a/assets/l10n/zh/order_attribute.yaml b/assets/l10n/zh/order_attribute.yaml index 7f08d188..26b21b17 100644 --- a/assets/l10n/zh/order_attribute.yaml +++ b/assets/l10n/zh/order_attribute.yaml @@ -15,6 +15,24 @@ tutorial: content: |- 這裡是用來設定顧客的資訊,例如:內用、外帶、上班族等。 這些資訊可以幫助我們統計哪些人來消費,進而做出更好的經營策略。 + createExample: 幫助建立一份範例以供測試。 +example: + age: 年齡 + _age: + $prefix: age + child: 小孩 + adult: 成人 + senior: 長者 + place: 位置 + _place: + $prefix: place + takeout: 外帶 + dineIn: 內用 + ecoFriendly: 環保 + _ecoFriendly: + $prefix: ecoFriendly + reusableBottle: 環保杯 + reusableBag: 環保袋 meta: mode: 種類:{name} default: 預設:{name} diff --git a/assets/l10n/zh/transit.yaml b/assets/l10n/zh/transit.yaml index 56a1d67f..b6ab5443 100644 --- a/assets/l10n/zh/transit.yaml +++ b/assets/l10n/zh/transit.yaml @@ -1,12 +1,6 @@ $prefix: "transit" title: 資料轉移 description: 匯入、匯出店家資訊和訂單 -tutorial: - title: 同步多台裝置 - content: |- - 這裡是用來匯入匯出菜單、庫存、訂單記錄等資訊的地方。 - - 我們提供了 Google 試算表和純文字兩種方式,讓您可以方便地在不同裝置間同步資料。 method: title: 請選擇欲轉移的方式 name: diff --git a/lib/helpers/setup_example.dart b/lib/helpers/setup_example.dart index f2c00c84..11523ea5 100644 --- a/lib/helpers/setup_example.dart +++ b/lib/helpers/setup_example.dart @@ -2,7 +2,11 @@ import 'dart:developer'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/objects/menu_object.dart'; +import 'package:possystem/models/objects/order_attribute_object.dart'; +import 'package:possystem/models/order/order_attribute.dart'; +import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/ingredient.dart'; @@ -208,3 +212,49 @@ Future setupExampleMenu() async { await Menu.instance.addItem(e); } } + +Future setupExampleOrderAttrs() async { + if (OrderAttributes.instance.isNotEmpty) return; + + log('setting order attributes', name: 'example order attrs'); + for (final e in [ + OrderAttribute( + id: 'age', + name: S.orderAttributeExampleAge, + index: 1, + mode: OrderAttributeMode.statOnly, + options: { + 'child': OrderAttributeOption(id: 'child', name: '${S.orderAttributeExampleAgeChild} (0-12)', index: 1), + 'adult': OrderAttributeOption( + id: 'adult', name: '${S.orderAttributeExampleAgeAdult} (13-60)', index: 2, isDefault: true), + 'senior': OrderAttributeOption(id: 'senior', name: '${S.orderAttributeExampleAgeSenior} (60+)', index: 3), + }, + ), + OrderAttribute( + id: 'place', + name: S.orderAttributeExamplePlace, + index: 2, + mode: OrderAttributeMode.changeDiscount, + options: { + 'takeout': + OrderAttributeOption(id: 'takeout', name: S.orderAttributeExamplePlaceTakeout, index: 1, isDefault: true), + 'dine-in': + OrderAttributeOption(id: 'dine-in', name: S.orderAttributeExamplePlaceDineIn, index: 2, modeValue: 1.1), + }, + ), + OrderAttribute( + id: 'eco-friendly', + name: S.orderAttributeExampleEcoFriendly, + index: 3, + mode: OrderAttributeMode.changePrice, + options: { + 'reuseable-bag': OrderAttributeOption( + id: 'reuseable-bag', name: S.orderAttributeExampleEcoFriendlyReusableBag, index: 1, modeValue: -5), + 'reuseable-bottle': OrderAttributeOption( + id: 'reuseable-bottle', name: S.orderAttributeExampleEcoFriendlyReusableBottle, index: 1, modeValue: -30), + }, + ), + ]) { + await OrderAttributes.instance.addItem(e); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 97c5b611..cd5f6e13 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2024-06-27T05:21:06.324318Z", + "@@last_modified": "2024-07-06T10:00:50.953962Z", "@@author": "Lu Shueh Chou", "settingTab": "Settings", "settingVersion": "Version: {version}", @@ -213,8 +213,6 @@ "stockQuantityProportionHelper": "Applied when this quantity is used for an ingredient.\n\nFor example:\nif this quantity is \"Large\" and the default ratio is \"1.5\",\nand there's a product \"Cheeseburger\" with the ingredient \"Cheese,\"\nwhich uses \"2\" units of cheese per burger,\nwhen adding this quantity,\nthe quantity of \"Cheese\" will automatically be set to \"3\" (2 * 1.5).\n\nIf set to \"1,\" there's no effect.\n\nIf set to \"0,\" the ingredient won't be used.", "transitTitle": "Data Transfer", "transitDescription": "Importing and Exporting Store Information and Orders", - "transitTutorialTitle": "Sync Multiple Devices", - "transitTutorialContent": "This is where you can import/export menu, inventory, order records, and other information.\n\nWe provide two methods: Google Sheets and plain text, making it convenient to sync data across different devices.", "transitMethodTitle": "Please Select Transfer Method", "transitMethodName": "{name, select, googleSheet{Google Sheets} plainText{Plain Text} other{UNKNOWN}}", "@transitMethodName": { @@ -1158,6 +1156,17 @@ }, "orderAttributeTutorialTitle": "Customer Settings", "orderAttributeTutorialContent": "This is where you set customer information, such as dine-in, takeout, office worker, etc.\nThis information helps us track who comes to consume and make better business strategies.", + "orderAttributeTutorialCreateExample": "Help create an example to test.", + "orderAttributeExampleAge": "Age", + "orderAttributeExampleAgeChild": "Child", + "orderAttributeExampleAgeAdult": "Adult", + "orderAttributeExampleAgeSenior": "Senior", + "orderAttributeExamplePlace": "Place", + "orderAttributeExamplePlaceTakeout": "Takeout", + "orderAttributeExamplePlaceDineIn": "Dine-in", + "orderAttributeExampleEcoFriendly": "Eco-Friendly", + "orderAttributeExampleEcoFriendlyReusableBottle": "Reusable Bottle", + "orderAttributeExampleEcoFriendlyReusableBag": "Reusable Bag", "orderAttributeMetaMode": "Mode: {name}", "@orderAttributeMetaMode": { "placeholders": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b2fc7da6..b6ad7d04 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "@@last_modified": "2024-06-27T05:21:06.347696Z", + "@@last_modified": "2024-07-06T10:00:50.976445Z", "@@author": "Lu Shueh Chou", "settingTab": "設定", "settingVersion": "版本:{version}", @@ -213,8 +213,6 @@ "stockQuantityProportionHelper": "當產品成分使用此份量時,預設替該成分增加的比例。\n\n例如:此份量為「多量」預設份量為「1.5」,\n今有一產品「起司漢堡」的成分「起司」,每份漢堡會使用「2」單位的起司,\n當增加此份量時,則會自動替「起司」設定為「3」(2 * 1.5)的份量。\n\n若設為「1」則無任何影響。\n\n若設為「0」則代表將不會使用此成分", "transitTitle": "資料轉移", "transitDescription": "匯入、匯出店家資訊和訂單", - "transitTutorialTitle": "同步多台裝置", - "transitTutorialContent": "這裡是用來匯入匯出菜單、庫存、訂單記錄等資訊的地方。\n\n我們提供了 Google 試算表和純文字兩種方式,讓您可以方便地在不同裝置間同步資料。", "transitMethodTitle": "請選擇欲轉移的方式", "transitMethodName": "{name, select, googleSheet{Google 試算表} plainText{純文字} other{UNKNOWN}}", "@transitMethodName": { @@ -1158,6 +1156,17 @@ }, "orderAttributeTutorialTitle": "顧客設定", "orderAttributeTutorialContent": "這裡是用來設定顧客的資訊,例如:內用、外帶、上班族等。\n這些資訊可以幫助我們統計哪些人來消費,進而做出更好的經營策略。", + "orderAttributeTutorialCreateExample": "幫助建立一份範例以供測試。", + "orderAttributeExampleAge": "年齡", + "orderAttributeExampleAgeChild": "小孩", + "orderAttributeExampleAgeAdult": "成人", + "orderAttributeExampleAgeSenior": "長者", + "orderAttributeExamplePlace": "位置", + "orderAttributeExamplePlaceTakeout": "外帶", + "orderAttributeExamplePlaceDineIn": "內用", + "orderAttributeExampleEcoFriendly": "環保", + "orderAttributeExampleEcoFriendlyReusableBottle": "環保杯", + "orderAttributeExampleEcoFriendlyReusableBag": "環保袋", "orderAttributeMetaMode": "種類:{name}", "@orderAttributeMetaMode": { "placeholders": { diff --git a/lib/ui/home/setting_view.dart b/lib/ui/home/setting_view.dart index ff18c7a0..d9636a50 100644 --- a/lib/ui/home/setting_view.dart +++ b/lib/ui/home/setting_view.dart @@ -26,12 +26,14 @@ class SettingView extends StatefulWidget { class _SettingViewState extends State with AutomaticKeepAliveClientMixin { late final TutorialInTab? tab; - final GlobalKey<_TutorialCreateExampleMenuListTileState> _tutorialCheckbox = GlobalKey(); + final GlobalKey<_TutorialCheckboxListTileState> _tutorialOrderAttrs = GlobalKey(); + final GlobalKey<_TutorialCheckboxListTileState> _tutorialMenu = GlobalKey(); @override Widget build(BuildContext context) { super.build(context); + var orderAttrIndex = OrderAttributes.instance.isEmpty ? (Menu.instance.isEmpty ? 1 : 0) : -1; return TutorialWrapper( tab: tab, child: Scaffold( @@ -39,8 +41,7 @@ class _SettingViewState extends State with AutomaticKeepAliveClient ? null : Tutorial( id: 'home.order', - // 0 if menu tutorial is not shown (only index 0 will trigger tutorial) - index: Menu.instance.isNotEmpty ? 0 : 1, + index: orderAttrIndex + 1, spotlightBuilder: const SpotlightRectBuilder(borderRadius: 16.0), title: S.orderTutorialTitle, message: S.orderTutorialContent, @@ -68,11 +69,11 @@ class _SettingViewState extends State with AutomaticKeepAliveClient index: 0, title: S.menuTutorialTitle, message: S.menuTutorialContent, - below: _TutorialCreateExampleMenuListTile(key: _tutorialCheckbox), + below: _TutorialCheckboxListTile(key: _tutorialMenu, title: S.menuTutorialCreateExample), spotlightBuilder: const SpotlightRectBuilder(), disable: Menu.instance.isNotEmpty, action: () async { - if (_tutorialCheckbox.currentState?.createExampleMenu == true) { + if (_tutorialMenu.currentState?.value == true) { await setupExampleMenu(); } }, @@ -84,26 +85,26 @@ class _SettingViewState extends State with AutomaticKeepAliveClient subtitle: S.menuSubtitle, ), ), - Tutorial( - id: 'home.exporter', - index: 3, - title: S.transitTutorialTitle, - message: S.transitTutorialContent, - spotlightBuilder: const SpotlightRectBuilder(), - child: _buildRouteTile( - id: 'exporter', - icon: Icons.upload_file_sharp, - route: Routes.transit, - title: S.transitTitle, - subtitle: S.transitDescription, - ), + _buildRouteTile( + id: 'transit', + icon: Icons.upload_file_sharp, + route: Routes.transit, + title: S.transitTitle, + subtitle: S.transitDescription, ), Tutorial( id: 'home.order_attr', - index: 2, + index: orderAttrIndex, title: S.orderAttributeTutorialTitle, message: S.orderAttributeTutorialContent, + below: _TutorialCheckboxListTile(key: _tutorialOrderAttrs, title: S.orderAttributeTutorialCreateExample), spotlightBuilder: const SpotlightRectBuilder(), + disable: OrderAttributes.instance.isNotEmpty, + action: () async { + if (_tutorialOrderAttrs.currentState?.value == true) { + await setupExampleOrderAttrs(); + } + }, child: _buildRouteTile( id: 'order_attrs', icon: Icons.assignment_ind_sharp, @@ -149,9 +150,6 @@ class _SettingViewState extends State with AutomaticKeepAliveClient ); } - @override - bool get wantKeepAlive => true; - @override void initState() { tab = widget.tabIndex == null ? null : TutorialInTab(index: widget.tabIndex!, context: context); @@ -159,6 +157,9 @@ class _SettingViewState extends State with AutomaticKeepAliveClient super.initState(); } + @override + bool get wantKeepAlive => true; + Widget _buildRouteTile({ required String id, required IconData icon, @@ -265,25 +266,27 @@ class _HeaderInfoList extends StatelessWidget { } } -class _TutorialCreateExampleMenuListTile extends StatefulWidget { - const _TutorialCreateExampleMenuListTile({super.key}); +class _TutorialCheckboxListTile extends StatefulWidget { + final String title; + + const _TutorialCheckboxListTile({super.key, required this.title}); @override - State<_TutorialCreateExampleMenuListTile> createState() => _TutorialCreateExampleMenuListTileState(); + State<_TutorialCheckboxListTile> createState() => _TutorialCheckboxListTileState(); } -class _TutorialCreateExampleMenuListTileState extends State<_TutorialCreateExampleMenuListTile> { - bool createExampleMenu = true; +class _TutorialCheckboxListTileState extends State<_TutorialCheckboxListTile> { + bool value = true; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: CheckboxListTile( - value: createExampleMenu, - onChanged: (v) => setState(() => createExampleMenu = v!), + value: value, + onChanged: (v) => setState(() => value = v!), tileColor: Theme.of(context).primaryColor, - title: Text(S.menuTutorialCreateExample, style: const TextStyle(color: Colors.white)), + title: Text(widget.title, style: const TextStyle(color: Colors.white)), ), ); } diff --git a/test/helpers/setup_example_test.dart b/test/helpers/setup_example_test.dart index afd1770d..1e115190 100644 --- a/test/helpers/setup_example_test.dart +++ b/test/helpers/setup_example_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:possystem/helpers/setup_example.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stock.dart'; @@ -14,9 +15,11 @@ void main() { when(storage.add(any, any, any)).thenAnswer((_) => Future.value()); await setupExampleMenu(); + await setupExampleOrderAttrs(); verify(storage.add(any, any, any)); await setupExampleMenu(); + await setupExampleOrderAttrs(); verifyNever(storage.add(any, any, any)); }); @@ -26,6 +29,7 @@ void main() { Menu(); Stock(); Quantities(); + OrderAttributes(); }); }); } diff --git a/test/ui/home/home_page_test.dart b/test/ui/home/home_page_test.dart index 0df3b3f5..c387726f 100644 --- a/test/ui/home/home_page_test.dart +++ b/test/ui/home/home_page_test.dart @@ -101,7 +101,7 @@ void main() { // rest await navAndPop('setting.debug', 'debug.list'); await navAndPop('setting.menu', 'menu.search'); - await navAndPop('setting.exporter', 'transit.google_sheet'); + await navAndPop('setting.transit', 'transit.google_sheet'); await navAndPop('setting.quantity', 'quantity.add'); await navAndPop('setting.order_attrs', 'order_attributes.reorder'); await dragDown(); @@ -114,83 +114,79 @@ void main() { await navAndCheck('home.analysis', 'anal.history'); }); - testWidgets('Setup example menu', (tester) async { - when(cache.get(any)).thenReturn(null); - when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); + group('example menu', () { + setUp(() { + reset(cache); + when(cache.get(any)).thenReturn(null); + when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); + }); + + Widget buildApp() { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: SettingsProvider.instance), + ChangeNotifierProvider.value(value: Menu()), + ChangeNotifierProvider.value(value: Stock()), + ChangeNotifierProvider.value(value: Quantities()), + ChangeNotifierProvider.value(value: OrderAttributes()), + ], + child: MaterialApp.router( + routerConfig: GoRouter(observers: [ + MyApp.routeObserver + ], routes: [ + GoRoute( + path: '/', + routes: Routes.routes, + builder: (_, __) => const HomePage(tab: HomeTab.setting), + ) + ]), + theme: AppThemes.lightTheme, + darkTheme: AppThemes.darkTheme, + ), + ); + } - await tester.pumpWidget(MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: SettingsProvider.instance), - ChangeNotifierProvider.value(value: Menu()), - ChangeNotifierProvider.value(value: Stock()), - ChangeNotifierProvider.value(value: Quantities()), - ChangeNotifierProvider.value(value: OrderAttributes()), - ], - child: MaterialApp.router( - routerConfig: GoRouter(observers: [ - MyApp.routeObserver - ], routes: [ - GoRoute( - path: '/', - routes: Routes.routes, - builder: (_, __) => const HomePage(tab: HomeTab.setting), - ) - ]), - theme: AppThemes.lightTheme, - darkTheme: AppThemes.darkTheme, - ), - )); - await tester.pumpAndSettle(); - await tester.pump(const Duration(milliseconds: 5)); + Future startTutorial(WidgetTester tester) async { + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 5)); + } - // set up example menu - await tester.tapAt(Offset.zero); - await tester.pump(const Duration(milliseconds: 5)); - await tester.pump(const Duration(milliseconds: 5)); + Future goNext(WidgetTester tester) async { + await tester.tapAt(Offset.zero); + await tester.pump(const Duration(milliseconds: 5)); + await tester.pump(const Duration(milliseconds: 5)); + } - expect(find.text(S.orderTutorialTitle), findsOneWidget); - expect(Menu.instance.isNotEmpty, isTrue); - verify(cache.set('tutorial.home.menu', true)); - }); + testWidgets('Setup', (tester) async { + await tester.pumpWidget(buildApp()); + expect(Menu.instance.isEmpty, isTrue); + expect(OrderAttributes.instance.isEmpty, isTrue); - testWidgets('Disable example menu', (tester) async { - when(cache.get(any)).thenReturn(null); - when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); + await startTutorial(tester); + await goNext(tester); - await tester.pumpWidget(MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: SettingsProvider.instance), - ChangeNotifierProvider.value(value: Menu()), - ChangeNotifierProvider.value(value: Stock()), - ChangeNotifierProvider.value(value: Quantities()), - ChangeNotifierProvider.value(value: OrderAttributes()), - ], - child: MaterialApp.router( - routerConfig: GoRouter(observers: [ - MyApp.routeObserver - ], routes: [ - GoRoute( - path: '/', - routes: Routes.routes, - builder: (_, __) => const HomePage(tab: HomeTab.setting), - ) - ]), - theme: AppThemes.lightTheme, - darkTheme: AppThemes.darkTheme, - ), - )); - await tester.pumpAndSettle(); - await tester.pump(const Duration(milliseconds: 5)); - - // set up example menu - await tester.tap(find.text(S.menuTutorialCreateExample)); - await tester.tapAt(Offset.zero); - await tester.pump(const Duration(milliseconds: 5)); - await tester.pump(const Duration(milliseconds: 5)); - - expect(find.text(S.orderTutorialTitle), findsOneWidget); - expect(Menu.instance.isNotEmpty, isFalse); - verify(cache.set('tutorial.home.menu', true)); + expect(find.text(S.orderAttributeTutorialContent), findsOneWidget); + expect(Menu.instance.isNotEmpty, isTrue); + verify(cache.set('tutorial.home.menu', true)); + + await goNext(tester); + + expect(find.text(S.orderTutorialTitle), findsOneWidget); + expect(OrderAttributes.instance.isNotEmpty, isTrue); + verify(cache.set('tutorial.home.order_attr', true)); + }); + + testWidgets('Disable example menu', (tester) async { + await tester.pumpWidget(buildApp()); + + await startTutorial(tester); + await tester.tap(find.text(S.menuTutorialCreateExample)); + await goNext(tester); + + expect(find.text(S.orderAttributeTutorialContent), findsOneWidget); + expect(Menu.instance.isNotEmpty, isFalse); + verify(cache.set('tutorial.home.menu', true)); + }); }); setUp(() {