From 65cb790a3a802872439497256f0b6e4ccc9bf968 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Mar 2024 00:46:30 +0800 Subject: [PATCH 1/5] chore: add local_hero dependency --- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index a6069122..bdb457f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -685,6 +685,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + local_hero: + dependency: "direct main" + description: + name: local_hero + sha256: "5c85451dd51ecd0e8d3656775fac9a6db82f296f200d9931217186d34fed6089" + url: "https://pub.dev" + source: hosted + version: "0.3.0" local_settings: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index dd3028fe..03a2701e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: image_picker: ^1.0.7 intl: ^0.18.1 json_annotation: ^4.8.1 + local_hero: ^0.3.0 local_settings: ^0.3.1 mask_text_input_formatter: ^2.8.0 material_symbols_icons: ^4.2719.1 From 3319692225c6426a4a862acae24d73b8e34d3a30 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Mar 2024 00:47:01 +0800 Subject: [PATCH 2/5] chore: add preset constructor, accept predefined UUID --- lib/data/setup/default_accounts.dart | 12 +++--- lib/data/setup/default_categories.dart | 60 +++++++++++++------------- lib/entity/account.dart | 10 +++++ lib/entity/category.dart | 7 +++ 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/lib/data/setup/default_accounts.dart b/lib/data/setup/default_accounts.dart index 0eaca823..3d042354 100644 --- a/lib/data/setup/default_accounts.dart +++ b/lib/data/setup/default_accounts.dart @@ -5,23 +5,23 @@ import 'package:material_symbols_icons/symbols.dart'; List getAccountPresets(String currency) { return [ - Account( - id: -1, + Account.preset( name: "setup.accounts.preset.main".tr(), currency: currency, iconCode: FlowIconData.icon(Symbols.credit_card_rounded).toString(), + uuid: "864df1dc-fe59-47e0-8423-98d8f86453b6", ), - Account( - id: -1, + Account.preset( name: "setup.accounts.preset.cash".tr(), currency: currency, iconCode: FlowIconData.icon(Symbols.payments_rounded).toString(), + uuid: "d7ef9672-256b-4097-a55a-27a58c6f5ba5", ), - Account( - id: -1, + Account.preset( name: "setup.accounts.preset.savings".tr(), currency: currency, iconCode: FlowIconData.icon(Symbols.savings_rounded).toString(), + uuid: "c04e1cdd-842f-48c1-9c6c-d07fb2b09193", ), ]; } diff --git a/lib/data/setup/default_categories.dart b/lib/data/setup/default_categories.dart index 8a650eeb..6d5c86cc 100644 --- a/lib/data/setup/default_categories.dart +++ b/lib/data/setup/default_categories.dart @@ -5,82 +5,82 @@ import 'package:material_symbols_icons/symbols.dart'; List getCategoryPresets() { return [ - Category( - id: -1, + Category.preset( + uuid: "f38b4e03-2ce6-4605-aeb9-a5e5fa6d01f5", name: "setup.categories.preset.eatingOut".tr(), iconCode: const IconFlowIcon(Symbols.restaurant_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "9ee43092-34bb-4647-ba43-59b23ba69afe", name: "setup.categories.preset.groceries".tr(), iconCode: const IconFlowIcon(Symbols.grocery_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "fea4be2c-96cd-4844-a953-022a966985ee", name: "setup.categories.preset.drinks".tr(), iconCode: const IconFlowIcon(Symbols.local_cafe_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "dd3aacb3-35a9-4b04-b1f3-9fb3f58f1332", name: "setup.categories.preset.education".tr(), iconCode: const IconFlowIcon(Symbols.school_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "39bfdc73-4cba-4980-ba0d-c200f903cc97", name: "setup.categories.preset.health".tr(), iconCode: const IconFlowIcon(Symbols.health_and_safety_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "1a67735a-561a-48c0-bf86-f19dab4b95b5", name: "setup.categories.preset.transport".tr(), iconCode: const IconFlowIcon(Symbols.train_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "92e5e684-0bbf-4456-b731-2ad945d5773b", name: "setup.categories.preset.petrol".tr(), iconCode: const IconFlowIcon(Symbols.local_gas_station_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "c04b893b-bea8-4df3-804c-8b14e3e65c6d", name: "setup.categories.preset.shopping".tr(), iconCode: const IconFlowIcon(Symbols.shopping_cart_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "4c75d6c4-aed2-4f60-9d28-4b3ae55b4498", name: "setup.categories.preset.entertainment".tr(), iconCode: const IconFlowIcon(Symbols.sports_basketball_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "9555eecf-7570-4118-89b0-e7343ece6572", name: "setup.categories.preset.onlineServices".tr(), iconCode: const IconFlowIcon(Symbols.cloud_circle_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "e8cf1c76-cdf7-41e1-9343-923b86cd9ea2", name: "setup.categories.preset.gifts".tr(), iconCode: const IconFlowIcon(Symbols.featured_seasonal_and_gifts_rounded) .toString(), ), - Category( - id: -1, + Category.preset( + uuid: "f442d114-b8c0-4f7e-befd-70844ad16fb4", name: "setup.categories.preset.rent".tr(), iconCode: const IconFlowIcon(Symbols.request_quote_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "a535e3ac-2103-40d6-acb7-0c664eb3bf6e", name: "setup.categories.preset.utils".tr(), iconCode: const IconFlowIcon(Symbols.valve_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "4213e196-8974-41d4-8eb9-b4debf3118aa", name: "setup.categories.preset.taxes".tr(), iconCode: const IconFlowIcon(Symbols.account_balance_rounded).toString(), ), - Category( - id: -1, + Category.preset( + uuid: "8bec1ea1-726f-4228-9d14-d210e86a9586", name: "setup.categories.preset.paychecks".tr(), iconCode: const IconFlowIcon(Symbols.wallet_rounded).toString(), ), diff --git a/lib/entity/account.dart b/lib/entity/account.dart index 22d2706d..944b8179 100644 --- a/lib/entity/account.dart +++ b/lib/entity/account.dart @@ -72,6 +72,16 @@ class Account implements EntityBase { }) : createdDate = createdDate ?? DateTime.now(), uuid = const Uuid().v4(); + Account.preset({ + required this.name, + required this.currency, + required this.iconCode, + required this.uuid, + }) : excludeFromTotalBalance = false, + sortOrder = -1, + id = -1, + createdDate = DateTime.now(); + factory Account.fromJson(Map json) => _$AccountFromJson(json); Map toJson() => _$AccountToJson(this); diff --git a/lib/entity/category.dart b/lib/entity/category.dart index 256afae5..ed040a96 100644 --- a/lib/entity/category.dart +++ b/lib/entity/category.dart @@ -49,6 +49,13 @@ class Category implements EntityBase { }) : createdDate = createdDate ?? DateTime.now(), uuid = const Uuid().v4(); + Category.preset({ + required this.name, + required this.iconCode, + required this.uuid, + }) : createdDate = DateTime.now(), + id = -1; + factory Category.fromJson(Map json) => _$CategoryFromJson(json); Map toJson() => _$CategoryToJson(this); From edcdd931109f85ff77325b37849a1bd2b08c6fb0 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Mar 2024 00:47:40 +0800 Subject: [PATCH 3/5] chore: add plus/minus icon for preset items --- .../setup/accounts/account_preset_card.dart | 15 ++++++++++++--- .../setup/categories/category_preset_card.dart | 9 +++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/widgets/setup/accounts/account_preset_card.dart b/lib/widgets/setup/accounts/account_preset_card.dart index 9d5bd4ab..9a3371a5 100644 --- a/lib/widgets/setup/accounts/account_preset_card.dart +++ b/lib/widgets/setup/accounts/account_preset_card.dart @@ -4,11 +4,14 @@ import 'package:flow/theme/theme.dart'; import 'package:flow/widgets/general/flow_icon.dart'; import 'package:flow/widgets/general/surface.dart'; import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; class AccountPresetCard extends StatelessWidget { final Function(bool)? onSelect; final bool selected; + final bool preexisting; + final Account account; final BorderRadius borderRadius; @@ -18,14 +21,14 @@ class AccountPresetCard extends StatelessWidget { required this.account, required this.onSelect, required this.selected, + required this.preexisting, this.borderRadius = const BorderRadius.all(Radius.circular(24.0)), }); @override Widget build(BuildContext context) { - return AnimatedOpacity( + return Opacity( opacity: selected ? 1.0 : 0.46, - duration: const Duration(milliseconds: 200), child: Surface( shape: RoundedRectangleBorder(borderRadius: borderRadius), builder: (context) => InkWell( @@ -58,7 +61,13 @@ class AccountPresetCard extends StatelessWidget { style: context.textTheme.displaySmall, ), ], - ) + ), + if (!preexisting) ...[ + const Spacer(), + Icon( + selected ? Symbols.remove_rounded : Symbols.add_rounded, + ), + ], ], ), ], diff --git a/lib/widgets/setup/categories/category_preset_card.dart b/lib/widgets/setup/categories/category_preset_card.dart index bfa378b5..e0bf81fe 100644 --- a/lib/widgets/setup/categories/category_preset_card.dart +++ b/lib/widgets/setup/categories/category_preset_card.dart @@ -2,10 +2,12 @@ import 'package:flow/entity/category.dart'; import 'package:flow/utils/value_or.dart'; import 'package:flow/widgets/category_card.dart'; import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; class CategoryPresetCard extends StatelessWidget { final Function(bool) onSelect; final bool selected; + final bool preexisting; final Category category; @@ -14,17 +16,20 @@ class CategoryPresetCard extends StatelessWidget { required this.onSelect, required this.selected, required this.category, + required this.preexisting, }); @override Widget build(BuildContext context) { - return AnimatedOpacity( + return Opacity( opacity: selected ? 1.0 : 0.46, - duration: const Duration(milliseconds: 200), child: CategoryCard( category: category, onTapOverride: ValueOr(() => onSelect(!selected)), showAmount: false, + trailing: preexisting + ? null + : Icon(selected ? Symbols.remove_rounded : Symbols.add_rounded), ), ); } From 3ab3a7789c84785a2f3d098540d850ceb4958a70 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Mar 2024 00:48:00 +0800 Subject: [PATCH 4/5] add trailing on category card --- lib/widgets/category_card.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/widgets/category_card.dart b/lib/widgets/category_card.dart index 9f3e6210..ff30ff91 100644 --- a/lib/widgets/category_card.dart +++ b/lib/widgets/category_card.dart @@ -17,10 +17,13 @@ class CategoryCard extends StatelessWidget { final ValueOr? onTapOverride; + final Widget? trailing; + const CategoryCard({ super.key, required this.category, this.onTapOverride, + this.trailing, this.showAmount = true, this.borderRadius = const BorderRadius.all(Radius.circular(16.0)), }); @@ -58,6 +61,10 @@ class CategoryCard extends StatelessWidget { ], ), const Spacer(), + if (trailing != null) ...[ + trailing!, + const SizedBox(width: 12.0), + ], ], ), ), From 29138adc7841ba3175b9352a200c707358476fa6 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Mar 2024 00:48:25 +0800 Subject: [PATCH 5/5] chore: rehaul account/category page, improve others' ui --- assets/l10n/en_US.json | 13 +- assets/l10n/mn_MN.json | 11 +- lib/routes/setup/setup_accounts_page.dart | 87 +++++++++---- lib/routes/setup/setup_categories_page.dart | 116 ++++++++++++++---- lib/routes/setup/setup_currency_page.dart | 54 ++++++-- lib/routes/setup/setup_profile_page.dart | 23 ++-- .../setup/setup_profile_picture_page.dart | 15 ++- lib/widgets/general/info_text.dart | 10 +- lib/widgets/setup/setup_header.dart | 19 --- 9 files changed, 239 insertions(+), 109 deletions(-) delete mode 100644 lib/widgets/setup/setup_header.dart diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index 01d3ed62..a0086137 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -27,19 +27,20 @@ "setup.slides.offline": "You own your data", "setup.slides.offline.description": "All of your data is stored only on-device, with full export options available", "setup.profile.setup": "What's your name?", - "setup.profile.addPhoto": "Add a profile photo", + "setup.profile.addPhoto": "Add a photo", "setup.profile.addPhoto.skip": "Skip", - "setup.primaryCurrency.setup": "What currency do you primarily use?", - "setup.primaryCurrency.choose": "Choose primary currency", + "setup.profile.addPhoto.description": "This is optional. Your photo will be stored only on your device, and will not be included in backups.", + "setup.primaryCurrency.setup": "Select currency", + "setup.primaryCurrency.description": "This will be your primary currency. You can change this later in \"Preferences\" menu.", + "setup.primaryCurrency.choose": "Choose a currency", "setup.accounts.setup": "Setup accounts", - "setup.accounts.description": "Create new accounts, and/or add from the presets. You can change this later in the accounts tab.", + "setup.accounts.description": "Create new accounts, and/or add from the presets. You can change this later in the \"Accounts\" tab.", "setup.accounts.addAccount": "Add new account", - "setup.accounts.preset.description": "Click on a preset to add", "setup.accounts.preset.main": "Main", "setup.accounts.preset.cash": "Cash", "setup.accounts.preset.savings": "Savings", "setup.categories.setup": "Setup categories", - "setup.categories.preset.description": "Click on a preset to add", + "setup.categories.description": "Create categories, and/or add from the presets. You can change this later in \"Profile > Categories\" menu.", "setup.categories.preset.eatingOut": "Eating out", "setup.categories.preset.groceries": "Groceries", "setup.categories.preset.drinks": "Drinks & Beverages", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index 5a79da7b..d387cf15 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -27,19 +27,20 @@ "setup.slides.offline": "Таны мэдээлэл таны мэдэлд", "setup.slides.offline.description": "Хэрэглэгчийн бүх мэдээлэл зөвхөн төхөөрөмж дээр хадгалагддаг ба хүссэн үедээ нөөцөлж авах боломжтой", "setup.profile.setup": "Таныг хэн гэдэг вэ?", - "setup.profile.addPhoto": "Нүүр зураг нэмэх", + "setup.profile.addPhoto": "Зураг нэмэх", "setup.profile.addPhoto.skip": "Алгасах", - "setup.primaryCurrency.setup": "Та ямар валют голчлон ашигладаг вэ?", + "setup.profile.addPhoto.description": "Зураг оруулах шаардлагагүй. Таны зураг зөвхөн таны төхөөрөмж дээр хадгалагдах бөгөөд нөөцлөх үед хамт нөөцлөгдөхгүй.", + "setup.primaryCurrency.setup": "Валют сонгох", + "setup.primaryCurrency.description": "Энэ таны үндсэн валют болох бөгөөд та үүнийг \"Тохиргоо\" цэсээс өөрчлөх боломжтой.", "setup.primaryCurrency.choose": "Үндсэн валют сонгох", "setup.accounts.setup": "Данс тохируулах", - "setup.accounts.description": "Шинэ данс үүсгэх, эсвэл доорх урьдчилан бэлдсэн данснуудаас нэмээрэй. Та дараа данснууд цэсээс үүнийг дахин тохируулах боломжтой.", + "setup.accounts.description": "Шинэ данс үүсгэх, эсвэл доорх сонголтуудаас нэмээрэй. Та үүнийг дараа \"Данснууд\" цэсээс тохируулах боломжтой.", "setup.accounts.addAccount": "Шинэ данс нэмэх", - "setup.accounts.preset.description": "Урьдчилан бэлдсэн данс дээр дарж нэмээрэй", "setup.accounts.preset.main": "Үндсэн", "setup.accounts.preset.cash": "Бэлэн мөнгө", "setup.accounts.preset.savings": "Хадгаламж", "setup.categories.setup": "Ангиллууд тохируулах", - "setup.categories.preset.description": "Урьдчилан бэлдсэн ангилал дээр дарж нэмээрэй", + "setup.categories.description": "Шинээр ангилал үүсгэх, эсвэл доорх сонголтуудаас нэмээрэй. Та үүнийг дараа \"Бүртгэл\" цэсээс тохируулах боломжтой.", "setup.categories.preset.eatingOut": "Гадуур хооллолт", "setup.categories.preset.groceries": "Хүнс", "setup.categories.preset.drinks": "Уух зүйлс", diff --git a/lib/routes/setup/setup_accounts_page.dart b/lib/routes/setup/setup_accounts_page.dart index d942dae8..b809b8cb 100644 --- a/lib/routes/setup/setup_accounts_page.dart +++ b/lib/routes/setup/setup_accounts_page.dart @@ -4,13 +4,14 @@ import 'package:flow/l10n/extensions.dart'; import 'package:flow/objectbox.dart'; import 'package:flow/objectbox/objectbox.g.dart'; import 'package:flow/prefs.dart'; +import 'package:flow/utils/utils.dart'; import 'package:flow/widgets/general/button.dart'; import 'package:flow/widgets/general/info_text.dart'; import 'package:flow/widgets/setup/accounts/account_preset_card.dart'; import 'package:flow/widgets/setup/accounts/add_account_card.dart'; -import 'package:flow/widgets/setup/setup_header.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:local_hero/local_hero.dart'; import 'package:material_symbols_icons/symbols.dart'; class SetupAccountsPage extends StatefulWidget { @@ -42,14 +43,16 @@ class _SetupAccountsPageState extends State { presetAccounts = getAccountPresets(primaryCurrency) .where((account) => !existingAccounts - .any((existingAccount) => existingAccount.name == account.name)) + .any((existingAccount) => existingAccount.uuid == account.uuid)) .toList(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + title: Text("setup.accounts.setup".t(context)), + ), body: SafeArea( child: StreamBuilder( stream: qb().watch(triggerImmediately: true), @@ -59,41 +62,52 @@ class _SetupAccountsPageState extends State { return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - SetupHeader("setup.accounts.setup".t(context)), + InfoText( + child: Text( + "setup.accounts.description".t(context), + ), + ), const SizedBox(height: 16.0), const AddAccountCard(), const SizedBox(height: 16.0), ...currentAccounts.map( - (e) => Padding( + (account) => Padding( padding: const EdgeInsets.only(bottom: 16.0), child: AccountPresetCard( - account: e, + key: ValueKey(account.uuid), + account: account, onSelect: null, selected: true, + preexisting: true, ), ), ), - const SizedBox(height: 16.0), - if (presetAccounts.isNotEmpty) ...[ - InfoText( - child: Text( - "setup.accounts.preset.description".t(context), - ), - ), - const SizedBox(height: 8.0), - ], - ...presetAccounts.indexed.map( - (e) => Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: AccountPresetCard( - account: e.$2, - onSelect: (selected) => select(e.$1, selected), - selected: e.$2.id == 0, - ), + LocalHeroScope( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + child: Column( + mainAxisSize: MainAxisSize.min, + children: presetAccounts + .map((preset) => Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: LocalHero( + key: ValueKey(preset.uuid), + tag: preset.uuid, + child: AccountPresetCard( + key: ValueKey(preset.uuid), + account: preset, + onSelect: (selected) => + select(preset.uuid, selected), + selected: preset.id == 0, + preexisting: false, + ), + ), + )) + .toList(), ), - ), + ) ], ), ); @@ -117,14 +131,27 @@ class _SetupAccountsPageState extends State { ); } - void select(int index, bool selected) { - presetAccounts[index].id = selected ? 0 : -1; + void loadPresets() {} + + void select(String uuid, bool selected) { + final Account? preset = + presetAccounts.firstWhereOrNull((element) => element.uuid == uuid); + + if (preset != null) { + preset.id = selected ? 0 : -1; + } + + presetAccounts.sort((a, b) => b.id.compareTo(a.id)); setState(() {}); } void save() async { if (busy) return; + setState(() { + busy = true; + }); + try { final List selectedAccounts = presetAccounts.where((element) => element.id == 0).toList(); @@ -135,6 +162,12 @@ class _SetupAccountsPageState extends State { await ObjectBox().box().putManyAsync(selectedAccounts); + presetAccounts.removeWhere((element) => + selectedAccounts.indexWhere( + (selected) => element.uuid == selected.uuid, + ) != + -1); + if (mounted) { context.push("/setup/categories"); } diff --git a/lib/routes/setup/setup_categories_page.dart b/lib/routes/setup/setup_categories_page.dart index ac667f51..7a81c7d3 100644 --- a/lib/routes/setup/setup_categories_page.dart +++ b/lib/routes/setup/setup_categories_page.dart @@ -11,9 +11,9 @@ import 'package:flow/widgets/category_card.dart'; import 'package:flow/widgets/general/info_text.dart'; import 'package:flow/widgets/setup/categories/category_preset_card.dart'; -import 'package:flow/widgets/setup/setup_header.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:local_hero/local_hero.dart'; import 'package:material_symbols_icons/symbols.dart'; class SetupCategoriesPage extends StatefulWidget { @@ -43,14 +43,21 @@ class _SetupCategoriesPageState extends State { presetCategories = getCategoryPresets() .where((category) => !existingCategories - .any((existingCategory) => existingCategory.name == category.name)) + .any((existingCategory) => existingCategory.uuid == category.uuid)) .toList(); + + // Select all in upon loading + for (final preset in presetCategories) { + preset.id = 0; + } } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + title: Text("setup.categories.setup".t(context)), + ), body: SafeArea( child: StreamBuilder( stream: qb().watch(triggerImmediately: true), @@ -58,12 +65,42 @@ class _SetupCategoriesPageState extends State { final List currentCategories = snapshot.data?.find() ?? []; + final Set presetSelections = + presetCategories.map((preset) => preset.id == 0).toSet(); + + final bool? presetSelectedAll = + presetSelections.length == 1 ? presetSelections.first : null; + return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SetupHeader("setup.categories.setup".t(context)), + InfoText( + child: Text("setup.categories.description".t(context))), + if (presetCategories.isNotEmpty) ...[ + const SizedBox(height: 8.0), + Align( + alignment: Alignment.topRight, + child: TextButton( + onPressed: selectAll, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("general.select.all".t(context)), + const SizedBox(width: 8.0), + IgnorePointer( + child: Checkbox.adaptive( + value: presetSelectedAll, + onChanged: (value) => (), + tristate: true, + ), + ) + ], + ), + ), + ), + ], const SizedBox(height: 16.0), const AddCategoryCard(), const SizedBox(height: 16.0), @@ -77,23 +114,29 @@ class _SetupCategoriesPageState extends State { ), ), ), - const SizedBox(height: 16.0), - if (presetCategories.isNotEmpty) ...[ - InfoText( - child: Text( - "setup.accounts.preset.description".t(context), - ), - ), - const SizedBox(height: 8.0), - ], - ...presetCategories.indexed.map( - (e) => Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: CategoryPresetCard( - category: e.$2, - onSelect: (selected) => select(e.$1, selected), - selected: e.$2.id == 0, - ), + LocalHeroScope( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + child: Column( + mainAxisSize: MainAxisSize.min, + children: presetCategories + .map( + (preset) => LocalHero( + key: ValueKey(preset.uuid), + tag: preset.uuid, + child: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: CategoryPresetCard( + category: preset, + onSelect: (selected) => + select(preset.uuid, selected), + selected: preset.id == 0, + preexisting: false, + ), + ), + ), + ) + .toList(), ), ), ], @@ -119,20 +162,47 @@ class _SetupCategoriesPageState extends State { ); } - void select(int index, bool selected) { - presetCategories[index].id = selected ? 0 : -1; + void select(String uuid, bool selected) { + final Category? preset = + presetCategories.firstWhereOrNull((element) => element.uuid == uuid); + + if (preset != null) { + preset.id = selected ? 0 : -1; + } + + presetCategories.sort((a, b) => b.id.compareTo(a.id)); setState(() {}); } + void selectAll() { + final bool select = presetCategories.any((element) => element.id == -1); + + for (int i = 0; i < presetCategories.length; i++) { + presetCategories[i].id = select ? 0 : -1; + } + + setState(() => {}); + } + void save() async { if (busy) return; + setState(() { + busy = true; + }); + try { final List selectedCategories = presetCategories.where((element) => element.id == 0).toList(); await ObjectBox().box().putManyAsync(selectedCategories); + presetCategories.removeWhere((element) => + selectedCategories.indexWhere( + (selected) => element.uuid == selected.uuid, + ) != + -1); + if (mounted) { GoRouter.of(context).popUntil((route) => route.path == "/setup"); diff --git a/lib/routes/setup/setup_currency_page.dart b/lib/routes/setup/setup_currency_page.dart index d05e1144..4ce7a32b 100644 --- a/lib/routes/setup/setup_currency_page.dart +++ b/lib/routes/setup/setup_currency_page.dart @@ -2,8 +2,8 @@ import 'package:flow/l10n/extensions.dart'; import 'package:flow/prefs.dart'; import 'package:flow/theme/theme.dart'; import 'package:flow/widgets/general/button.dart'; +import 'package:flow/widgets/general/info_text.dart'; import 'package:flow/widgets/select_currency_sheet.dart'; -import 'package:flow/widgets/setup/setup_header.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:go_router/go_router.dart'; @@ -17,8 +17,13 @@ class SetupCurrencyPage extends StatefulWidget { } class _SetupCurrencyPageState extends State { + final TextEditingController _textController = + TextEditingController(text: "~~~"); + String? _currency; + dynamic error; + @override void initState() { super.initState(); @@ -28,22 +33,47 @@ class _SetupCurrencyPageState extends State { }); } + @override + void dispose() { + _textController.dispose(); + + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), + appBar: AppBar(title: Text("setup.primaryCurrency.setup".t(context))), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( children: [ - SetupHeader("setup.primaryCurrency.setup".t(context)), - // SelectCurrencySheet + InfoText( + child: Text( + "setup.primaryCurrency.description".t(context), + ), + ), const SizedBox(height: 16.0), - Text( - _currency ?? "-", - style: context.textTheme.displayMedium, + TextField( + readOnly: true, + controller: _textController, + textInputAction: TextInputAction.send, + onSubmitted: (_) => save(), + decoration: const InputDecoration(border: InputBorder.none), + textAlign: TextAlign.center, + style: _currency == null + ? context.textTheme.displaySmall?.semi(context) + : context.textTheme.displaySmall, ), + if (error != null) ...[ + const SizedBox(height: 8.0), + Text( + error.toString(), + style: context.textTheme.bodyMedium + ?.copyWith(color: context.flowColors.expense), + ) + ], const SizedBox(height: 16.0), Button( child: Text("setup.primaryCurrency.choose".t(context)), @@ -78,12 +108,22 @@ class _SetupCurrencyPageState extends State { isScrollControlled: true, ); + _textController.text = result ?? _currency ?? "~~~"; + setState(() { _currency = result ?? _currency; }); } void save() { + if (_currency == null) { + error = "error.input.mustBeNotEmpty".t(context); + + setState(() {}); + + return; + } + LocalPreferences().primaryCurrency.set(_currency!); context.push("/setup/accounts"); diff --git a/lib/routes/setup/setup_profile_page.dart b/lib/routes/setup/setup_profile_page.dart index 4594183d..12df37bf 100644 --- a/lib/routes/setup/setup_profile_page.dart +++ b/lib/routes/setup/setup_profile_page.dart @@ -4,7 +4,6 @@ import 'package:flow/l10n/extensions.dart'; import 'package:flow/objectbox.dart'; import 'package:flow/objectbox/objectbox.g.dart'; import 'package:flow/widgets/general/button.dart'; -import 'package:flow/widgets/setup/setup_header.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -40,24 +39,20 @@ class _SetupProfilePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + title: Text("setup.profile.setup".t(context)), + ), body: SafeArea( child: Form( key: formKey, child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SetupHeader("setup.profile.setup".t(context)), - const SizedBox(height: 16.0), - TextFormField( - controller: _textEditingController, - autofocus: true, - validator: validateRequiredField, - textInputAction: TextInputAction.send, - onFieldSubmitted: (value) => save(), - ), - ], + child: TextFormField( + controller: _textEditingController, + autofocus: true, + validator: validateRequiredField, + textInputAction: TextInputAction.send, + onFieldSubmitted: (value) => save(), ), ), ), diff --git a/lib/routes/setup/setup_profile_picture_page.dart b/lib/routes/setup/setup_profile_picture_page.dart index e4869b06..e0ac19cd 100644 --- a/lib/routes/setup/setup_profile_picture_page.dart +++ b/lib/routes/setup/setup_profile_picture_page.dart @@ -6,8 +6,8 @@ import 'package:flow/l10n/extensions.dart'; import 'package:flow/objectbox.dart'; import 'package:flow/utils/utils.dart'; import 'package:flow/widgets/general/button.dart'; +import 'package:flow/widgets/general/info_text.dart'; import 'package:flow/widgets/general/profile_picture.dart'; -import 'package:flow/widgets/setup/setup_header.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -30,7 +30,9 @@ class _SetupProfilePhotoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), + appBar: AppBar( + title: Text("setup.profile.addPhoto".t(context)), + ), body: SafeArea( child: Align( alignment: Alignment.topCenter, @@ -40,8 +42,12 @@ class _SetupProfilePhotoPageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SetupHeader("setup.profile.addPhoto".t(context)), - const SizedBox(height: 16.0), + InfoText( + child: Text( + "setup.profile.addPhoto.description".t(context), + ), + ), + const SizedBox(height: 24.0), ProfilePicture( key: ValueKey(_profilePictureUpdateCounter), filePath: widget.profileImagePath, @@ -49,7 +55,6 @@ class _SetupProfilePhotoPageState extends State { showOverlayUponHover: true, size: MediaQuery.of(context).size.width * 0.5, ), - const SizedBox(height: 16.0), ], ), ), diff --git a/lib/widgets/general/info_text.dart b/lib/widgets/general/info_text.dart index 1cab7a14..86f5cf62 100644 --- a/lib/widgets/general/info_text.dart +++ b/lib/widgets/general/info_text.dart @@ -3,14 +3,18 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; class InfoText extends StatelessWidget { + /// Centers the text and icon vertically instead of top + final bool singleLine; + final Widget child; - const InfoText({super.key, required this.child}); + const InfoText({super.key, required this.child, this.singleLine = false}); @override Widget build(BuildContext context) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + singleLine ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ Icon( Symbols.info_rounded, @@ -18,7 +22,7 @@ class InfoText extends StatelessWidget { color: context.flowColors.semi, size: 16.0, ), - const SizedBox(width: 8.0), + const SizedBox(width: 4.0), Flexible( child: DefaultTextStyle( style: context.textTheme.bodySmall!.semi(context), diff --git a/lib/widgets/setup/setup_header.dart b/lib/widgets/setup/setup_header.dart deleted file mode 100644 index 88110db7..00000000 --- a/lib/widgets/setup/setup_header.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flow/theme/theme.dart'; -import 'package:flutter/material.dart'; - -class SetupHeader extends StatelessWidget { - final String text; - - const SetupHeader(this.text, {super.key}); - - @override - Widget build(BuildContext context) { - return Align( - alignment: AlignmentDirectional.topStart, - child: Text( - text, - style: context.textTheme.headlineSmall, - ), - ); - } -}