diff --git a/assets/icons/supported_selectable_icons/entertainment/casino.svg b/assets/icons/supported_selectable_icons/entertainment/casino.svg index 8df94fa3..347465f5 100644 --- a/assets/icons/supported_selectable_icons/entertainment/casino.svg +++ b/assets/icons/supported_selectable_icons/entertainment/casino.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/entertainment/golf_course.svg b/assets/icons/supported_selectable_icons/entertainment/golf_course.svg index 7a0641f4..1164ba39 100644 --- a/assets/icons/supported_selectable_icons/entertainment/golf_course.svg +++ b/assets/icons/supported_selectable_icons/entertainment/golf_course.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/entertainment/roller_skating.svg b/assets/icons/supported_selectable_icons/entertainment/roller_skating.svg index de8581d4..782e266a 100644 --- a/assets/icons/supported_selectable_icons/entertainment/roller_skating.svg +++ b/assets/icons/supported_selectable_icons/entertainment/roller_skating.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/food/dinner_dining.svg b/assets/icons/supported_selectable_icons/food/dinner_dining.svg index 3fd00730..50bd0955 100644 --- a/assets/icons/supported_selectable_icons/food/dinner_dining.svg +++ b/assets/icons/supported_selectable_icons/food/dinner_dining.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/food/fastfood.svg b/assets/icons/supported_selectable_icons/food/fastfood.svg index 4f445559..6bf7ab0e 100644 --- a/assets/icons/supported_selectable_icons/food/fastfood.svg +++ b/assets/icons/supported_selectable_icons/food/fastfood.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/food/kitchen.svg b/assets/icons/supported_selectable_icons/food/kitchen.svg index 9fb4072a..918c9c01 100644 --- a/assets/icons/supported_selectable_icons/food/kitchen.svg +++ b/assets/icons/supported_selectable_icons/food/kitchen.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/food/local_pizza.svg b/assets/icons/supported_selectable_icons/food/local_pizza.svg index a9f72c7b..71439c54 100644 --- a/assets/icons/supported_selectable_icons/food/local_pizza.svg +++ b/assets/icons/supported_selectable_icons/food/local_pizza.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/food/lunch_dining.svg b/assets/icons/supported_selectable_icons/food/lunch_dining.svg index 5fd8429d..05b99f56 100644 --- a/assets/icons/supported_selectable_icons/food/lunch_dining.svg +++ b/assets/icons/supported_selectable_icons/food/lunch_dining.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/food/nutrition.svg b/assets/icons/supported_selectable_icons/food/nutrition.svg index eac9722e..c4b7c1d7 100644 --- a/assets/icons/supported_selectable_icons/food/nutrition.svg +++ b/assets/icons/supported_selectable_icons/food/nutrition.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/medical/medication.svg b/assets/icons/supported_selectable_icons/medical/medication.svg index b890ab81..76a1e2ea 100644 --- a/assets/icons/supported_selectable_icons/medical/medication.svg +++ b/assets/icons/supported_selectable_icons/medical/medication.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/medical/self_care.svg b/assets/icons/supported_selectable_icons/medical/self_care.svg index 7141f2b7..31ec9e8c 100644 --- a/assets/icons/supported_selectable_icons/medical/self_care.svg +++ b/assets/icons/supported_selectable_icons/medical/self_care.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/medical/vaccines.svg b/assets/icons/supported_selectable_icons/medical/vaccines.svg new file mode 100644 index 00000000..e26b1788 --- /dev/null +++ b/assets/icons/supported_selectable_icons/medical/vaccines.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/money/payments.svg b/assets/icons/supported_selectable_icons/money/payments.svg index 081d20ae..aefed13e 100644 --- a/assets/icons/supported_selectable_icons/money/payments.svg +++ b/assets/icons/supported_selectable_icons/money/payments.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/money/receipt_long.svg b/assets/icons/supported_selectable_icons/money/receipt_long.svg index 3510e437..dd78ce31 100644 --- a/assets/icons/supported_selectable_icons/money/receipt_long.svg +++ b/assets/icons/supported_selectable_icons/money/receipt_long.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/money/redeem.svg b/assets/icons/supported_selectable_icons/money/redeem.svg index 2c9f3dbd..782bcaf8 100644 --- a/assets/icons/supported_selectable_icons/money/redeem.svg +++ b/assets/icons/supported_selectable_icons/money/redeem.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/app_badging.svg b/assets/icons/supported_selectable_icons/other/app_badging.svg index 55b5e604..56fbb821 100644 --- a/assets/icons/supported_selectable_icons/other/app_badging.svg +++ b/assets/icons/supported_selectable_icons/other/app_badging.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/content_cut.svg b/assets/icons/supported_selectable_icons/other/content_cut.svg new file mode 100644 index 00000000..5b9567ac --- /dev/null +++ b/assets/icons/supported_selectable_icons/other/content_cut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/inventory_2.svg b/assets/icons/supported_selectable_icons/other/inventory_2.svg index f20adbec..606b84da 100644 --- a/assets/icons/supported_selectable_icons/other/inventory_2.svg +++ b/assets/icons/supported_selectable_icons/other/inventory_2.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/iron.svg b/assets/icons/supported_selectable_icons/other/iron.svg index 4a3297e5..3677ba1d 100644 --- a/assets/icons/supported_selectable_icons/other/iron.svg +++ b/assets/icons/supported_selectable_icons/other/iron.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/pets.svg b/assets/icons/supported_selectable_icons/other/pets.svg index 1a5cd143..013d8f04 100644 --- a/assets/icons/supported_selectable_icons/other/pets.svg +++ b/assets/icons/supported_selectable_icons/other/pets.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/recycling.svg b/assets/icons/supported_selectable_icons/other/recycling.svg new file mode 100644 index 00000000..1c4165cb --- /dev/null +++ b/assets/icons/supported_selectable_icons/other/recycling.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/shopping_cart.svg b/assets/icons/supported_selectable_icons/other/shopping_cart.svg index 8dea2cfb..0f826757 100644 --- a/assets/icons/supported_selectable_icons/other/shopping_cart.svg +++ b/assets/icons/supported_selectable_icons/other/shopping_cart.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/smoking_rooms.svg b/assets/icons/supported_selectable_icons/other/smoking_rooms.svg new file mode 100644 index 00000000..6620ba8b --- /dev/null +++ b/assets/icons/supported_selectable_icons/other/smoking_rooms.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/sprinkler.svg b/assets/icons/supported_selectable_icons/other/sprinkler.svg index 7d05d011..7f698c2c 100644 --- a/assets/icons/supported_selectable_icons/other/sprinkler.svg +++ b/assets/icons/supported_selectable_icons/other/sprinkler.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/other/star.svg b/assets/icons/supported_selectable_icons/other/star.svg index a7408fe3..c1b8cd49 100644 --- a/assets/icons/supported_selectable_icons/other/star.svg +++ b/assets/icons/supported_selectable_icons/other/star.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/computer.svg b/assets/icons/supported_selectable_icons/technology/computer.svg index 379608e5..f35323d4 100644 --- a/assets/icons/supported_selectable_icons/technology/computer.svg +++ b/assets/icons/supported_selectable_icons/technology/computer.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/devices.svg b/assets/icons/supported_selectable_icons/technology/devices.svg index 0748d0ba..9df3409c 100644 --- a/assets/icons/supported_selectable_icons/technology/devices.svg +++ b/assets/icons/supported_selectable_icons/technology/devices.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/monitor.svg b/assets/icons/supported_selectable_icons/technology/monitor.svg new file mode 100644 index 00000000..5bfc3580 --- /dev/null +++ b/assets/icons/supported_selectable_icons/technology/monitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/phone_iphone.svg b/assets/icons/supported_selectable_icons/technology/phone_iphone.svg new file mode 100644 index 00000000..2e4610f1 --- /dev/null +++ b/assets/icons/supported_selectable_icons/technology/phone_iphone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/power.svg b/assets/icons/supported_selectable_icons/technology/power.svg new file mode 100644 index 00000000..287e8198 --- /dev/null +++ b/assets/icons/supported_selectable_icons/technology/power.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/print.svg b/assets/icons/supported_selectable_icons/technology/print.svg new file mode 100644 index 00000000..6fe66f01 --- /dev/null +++ b/assets/icons/supported_selectable_icons/technology/print.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/sd_card.svg b/assets/icons/supported_selectable_icons/technology/sd_card.svg new file mode 100644 index 00000000..ad1f4a52 --- /dev/null +++ b/assets/icons/supported_selectable_icons/technology/sd_card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/smartphone.svg b/assets/icons/supported_selectable_icons/technology/smartphone.svg new file mode 100644 index 00000000..b907b069 --- /dev/null +++ b/assets/icons/supported_selectable_icons/technology/smartphone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/technology/stadia_controller.svg b/assets/icons/supported_selectable_icons/technology/stadia_controller.svg index b3b7504b..62e024d7 100644 --- a/assets/icons/supported_selectable_icons/technology/stadia_controller.svg +++ b/assets/icons/supported_selectable_icons/technology/stadia_controller.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/airplane_ticket.svg b/assets/icons/supported_selectable_icons/transport/airplane_ticket.svg new file mode 100644 index 00000000..36ea1035 --- /dev/null +++ b/assets/icons/supported_selectable_icons/transport/airplane_ticket.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/car_rental.svg b/assets/icons/supported_selectable_icons/transport/car_rental.svg index 5443901c..509b3a73 100644 --- a/assets/icons/supported_selectable_icons/transport/car_rental.svg +++ b/assets/icons/supported_selectable_icons/transport/car_rental.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/car_repair.svg b/assets/icons/supported_selectable_icons/transport/car_repair.svg index 59d39a92..d94908f6 100644 --- a/assets/icons/supported_selectable_icons/transport/car_repair.svg +++ b/assets/icons/supported_selectable_icons/transport/car_repair.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/connecting_airports.svg b/assets/icons/supported_selectable_icons/transport/connecting_airports.svg new file mode 100644 index 00000000..b45a3c27 --- /dev/null +++ b/assets/icons/supported_selectable_icons/transport/connecting_airports.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/electric_scooter.svg b/assets/icons/supported_selectable_icons/transport/electric_scooter.svg index d7e7de80..a14341d4 100644 --- a/assets/icons/supported_selectable_icons/transport/electric_scooter.svg +++ b/assets/icons/supported_selectable_icons/transport/electric_scooter.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/flight.svg b/assets/icons/supported_selectable_icons/transport/flight.svg index d6ef403f..ddd1c9bf 100644 --- a/assets/icons/supported_selectable_icons/transport/flight.svg +++ b/assets/icons/supported_selectable_icons/transport/flight.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/flight_takeoff.svg b/assets/icons/supported_selectable_icons/transport/flight_takeoff.svg index b6573bf8..82aa016c 100644 --- a/assets/icons/supported_selectable_icons/transport/flight_takeoff.svg +++ b/assets/icons/supported_selectable_icons/transport/flight_takeoff.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/local_shipping.svg b/assets/icons/supported_selectable_icons/transport/local_shipping.svg index 9762b033..ddff649d 100644 --- a/assets/icons/supported_selectable_icons/transport/local_shipping.svg +++ b/assets/icons/supported_selectable_icons/transport/local_shipping.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/local_taxi.svg b/assets/icons/supported_selectable_icons/transport/local_taxi.svg index e9f04713..b1dea8f6 100644 --- a/assets/icons/supported_selectable_icons/transport/local_taxi.svg +++ b/assets/icons/supported_selectable_icons/transport/local_taxi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/luggage.svg b/assets/icons/supported_selectable_icons/transport/luggage.svg new file mode 100644 index 00000000..6bcac03f --- /dev/null +++ b/assets/icons/supported_selectable_icons/transport/luggage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/supported_selectable_icons/transport/transportation.svg b/assets/icons/supported_selectable_icons/transport/transportation.svg new file mode 100644 index 00000000..b95922c0 --- /dev/null +++ b/assets/icons/supported_selectable_icons/transport/transportation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/app/accounts/account_selector.dart b/lib/app/accounts/account_selector.dart index 0a076c2a..b405e1b4 100644 --- a/lib/app/accounts/account_selector.dart +++ b/lib/app/accounts/account_selector.dart @@ -255,7 +255,7 @@ class _AccountSelectorModalState extends State { ), if (widget.allowMultiSelection) ScrollableWithBottomGradient.buildPositionedGradient( - AppColors.of(context).modalBackground), + Theme.of(context).colorSchemeExtended.modalBackground), ]), ); } diff --git a/lib/app/accounts/account_type_selector.dart b/lib/app/accounts/account_type_selector.dart index b1b88694..8fb1470a 100644 --- a/lib/app/accounts/account_type_selector.dart +++ b/lib/app/accounts/account_type_selector.dart @@ -77,8 +77,9 @@ class MonekinFilterChip extends StatelessWidget { borderRadius: BorderRadius.circular(16), side: BorderSide( width: 1.25, - color: - isSelected ? AppColors.of(context).primary : Colors.transparent, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Colors.transparent, ), ), clipBehavior: Clip.hardEdge, @@ -93,7 +94,7 @@ class MonekinFilterChip extends StatelessWidget { accountType.icon, size: 28, color: isSelected - ? AppColors.of(context).primary + ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(height: 18), @@ -101,7 +102,7 @@ class MonekinFilterChip extends StatelessWidget { accountType.title(context), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: isSelected - ? AppColors.of(context).primary + ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurfaceVariant), ), Text(accountType.description(context), diff --git a/lib/app/accounts/all_accounts_balance.dart b/lib/app/accounts/all_accounts_balance.dart index 47997c3c..9ccad2a9 100644 --- a/lib/app/accounts/all_accounts_balance.dart +++ b/lib/app/accounts/all_accounts_balance.dart @@ -120,7 +120,8 @@ class _AllAccountBalancePageState extends State { children: [ CardWithHeader( title: t.stats.balance_by_account, - bodyPadding: const EdgeInsets.symmetric(vertical: 4), + subtitle: t.stats.balance_by_account_subtitle, + bodyPadding: const EdgeInsets.only(bottom: 0, top: 8), body: accounts.isEmpty ? emptyAccountsIndicator() : ListView.separated( @@ -129,6 +130,8 @@ class _AllAccountBalancePageState extends State { final accountWithMoney = accounts[index]; return ListTile( + titleAlignment: ListTileTitleAlignment.bottom, + minTileHeight: 56, leading: accountWithMoney.account.displayIcon(context), onTap: () => RouteUtils.pushRoute( @@ -156,7 +159,6 @@ class _AllAccountBalancePageState extends State { ], ), AnimatedProgressBar( - width: 6, value: min( max(accountWithMoney.money / totalMoney, 0), @@ -175,6 +177,7 @@ class _AllAccountBalancePageState extends State { const SizedBox(height: 16), CardWithHeader( title: t.stats.balance_by_currency, + subtitle: t.stats.balance_by_currency_subtitle, bodyPadding: const EdgeInsets.symmetric(vertical: 4), body: Builder(builder: (context) { final currenciesWithMoney = getCurrenciesWithMoney(accounts); @@ -189,6 +192,8 @@ class _AllAccountBalancePageState extends State { final currencyWithMoney = currenciesWithMoney[index]; return ListTile( + titleAlignment: ListTileTitleAlignment.bottom, + minTileHeight: 56, leading: StreamBuilder( stream: CurrencyService.instance.getCurrencyByCode( currencyWithMoney.currency.code), @@ -239,7 +244,6 @@ class _AllAccountBalancePageState extends State { ], ), AnimatedProgressBar( - width: 6, value: min( max(currencyWithMoney.money / totalMoney, 0), diff --git a/lib/app/accounts/all_accounts_page.dart b/lib/app/accounts/all_accounts_page.dart index 1e8db490..b58068df 100644 --- a/lib/app/accounts/all_accounts_page.dart +++ b/lib/app/accounts/all_accounts_page.dart @@ -101,7 +101,7 @@ class _AllAccountsPageState extends State { bgColor: AppColors.of(context).light, margin: const EdgeInsets.symmetric( horizontal: 0, vertical: 4), - borderRadius: 12, + borderRadius: BorderRadius.circular(12), child: ListTile( trailing: accounts.length > 1 ? ReorderableDragIcon( diff --git a/lib/app/accounts/details/account_details.dart b/lib/app/accounts/details/account_details.dart index c8463c9d..35e2aaa3 100644 --- a/lib/app/accounts/details/account_details.dart +++ b/lib/app/accounts/details/account_details.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart' as drift; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; +import 'package:monekin/app/accounts/account_form.dart'; import 'package:monekin/app/accounts/details/account_details_actions.dart'; import 'package:monekin/app/transactions/label_value_info_list.dart'; import 'package:monekin/app/transactions/transactions.page.dart'; @@ -11,7 +12,6 @@ import 'package:monekin/core/database/services/exchange-rate/exchange_rate_servi import 'package:monekin/core/database/services/transaction/transaction_service.dart'; import 'package:monekin/core/models/account/account.dart'; import 'package:monekin/core/models/transaction/transaction_status.enum.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; import 'package:monekin/core/presentation/widgets/card_with_header.dart'; import 'package:monekin/core/presentation/widgets/form_fields/date_form_field.dart'; @@ -106,6 +106,11 @@ class _AccountDetailsPageState extends State { children: [ CardWithHeader( title: 'Info', + footer: CardFooterWithSingleButton( + text: t.general.edit, + onButtonClick: () => RouteUtils.pushRoute( + context, AccountFormPage(account: account)), + ), body: LabelValueInfoList(items: [ LabelValueInfoListItem( value: Text( @@ -148,14 +153,36 @@ class _AccountDetailsPageState extends State { const SizedBox(height: 16), CardWithHeader( title: t.home.last_transactions, - onHeaderButtonClick: () { - RouteUtils.pushRoute( - context, - TransactionsPage( - filters: TransactionFilters( - accountsIDs: [widget.account.id])), - ); - }, + bodyPadding: + const EdgeInsets.symmetric(vertical: 6), + footer: StreamBuilder( + stream: TransactionService.instance + .countTransactions( + predicate: TransactionFilters( + status: TransactionStatus.notIn({ + TransactionStatus.pending, + TransactionStatus.voided + }), + accountsIDs: [widget.account.id], + ), + ), + builder: (context, snapshot) { + if (!snapshot.hasData || + snapshot.data!.numberOfRes < 5) { + return const SizedBox.shrink(); + } + + return CardFooterWithSingleButton( + onButtonClick: () { + RouteUtils.pushRoute( + context, + TransactionsPage( + filters: TransactionFilters( + accountsIDs: [widget.account.id], + )), + ); + }); + }), body: TransactionListComponent( heroTagBuilder: (tr) => 'account-details-page__tr-icon-${tr.id}', @@ -215,7 +242,7 @@ class _AccountDetailHeader extends SliverPersistentHeaderDelegate { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.of(context).surface, + color: Theme.of(context).colorScheme.surface, border: Border( bottom: BorderSide( width: 2, diff --git a/lib/app/budgets/budget_details_page.dart b/lib/app/budgets/budget_details_page.dart index d765623e..26a9f0a1 100644 --- a/lib/app/budgets/budget_details_page.dart +++ b/lib/app/budgets/budget_details_page.dart @@ -146,17 +146,18 @@ class _BudgetDetailsPageState extends State { body: BudgetEvolutionChart(budget: budget)), const SizedBox(height: 16), CardWithHeader( - title: t.stats.by_categories, - body: ChartByCategories( - filters: budget.trFilters, - datePeriodState: budget.periodState, + title: t.stats.by_categories, + body: ChartByCategories( + filters: budget.trFilters, + datePeriodState: budget.periodState, + ), + footer: CardFooterWithSingleButton( + onButtonClick: () => RouteUtils.pushRoute( + context, + const StatsPage(initialIndex: 1), ), - onHeaderButtonClick: () { - RouteUtils.pushRoute( - context, - const StatsPage(initialIndex: 1), - ); - }), + ), + ), ], ), ), diff --git a/lib/app/budgets/components/budget_evolution_chart.dart b/lib/app/budgets/components/budget_evolution_chart.dart index dbc8f5d5..02db14d7 100644 --- a/lib/app/budgets/components/budget_evolution_chart.dart +++ b/lib/app/budgets/components/budget_evolution_chart.dart @@ -43,7 +43,7 @@ class BudgetEvolutionChart extends StatelessWidget { @override Widget build(BuildContext context) { - final lineColor = AppColors.of(context).primary; + final lineColor = Theme.of(context).colorScheme.primary; final t = Translations.of(context); return SizedBox( @@ -84,7 +84,8 @@ class BudgetEvolutionChart extends StatelessWidget { ]), lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - getTooltipColor: (spot) => AppColors.of(context).surface, + getTooltipColor: (spot) => + Theme.of(context).colorScheme.surface, tooltipHorizontalAlignment: FLHorizontalAlignment.right, tooltipMargin: -10, getTooltipItems: (touchedSpots) { diff --git a/lib/app/categories/categories_list_page.dart b/lib/app/categories/categories_list_page.dart index 55ae5a78..46d50a54 100644 --- a/lib/app/categories/categories_list_page.dart +++ b/lib/app/categories/categories_list_page.dart @@ -101,7 +101,7 @@ class _CategoriesListPageState extends State { bgColor: AppColors.of(context).light, margin: const EdgeInsets.symmetric( horizontal: 0, vertical: 4), - borderRadius: 12, + borderRadius: BorderRadius.circular(12), child: ListTile( trailing: categories.length > 1 ? ReorderableDragIcon( diff --git a/lib/app/categories/form/icon_and_color_selector.dart b/lib/app/categories/form/icon_and_color_selector.dart index 00782b4f..ec046255 100644 --- a/lib/app/categories/form/icon_and_color_selector.dart +++ b/lib/app/categories/form/icon_and_color_selector.dart @@ -31,7 +31,7 @@ class IconAndColorSelector extends StatelessWidget { return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: AppColors.of(context).inputFill, + color: Theme.of(context).colorSchemeExtended.inputFill, borderRadius: BorderRadius.circular(12), ), child: Row( @@ -56,7 +56,7 @@ class IconAndColorSelector extends StatelessWidget { ), ); }, - bgColor: AppColors.of(context).inputFill, + bgColor: Theme.of(context).colorSchemeExtended.inputFill, child: ListTile( mouseCursor: SystemMouseCursors.click, title: Text(t.icon_selector.icon), @@ -64,7 +64,11 @@ class IconAndColorSelector extends StatelessWidget { const Icon(Icons.arrow_forward_ios_rounded, size: 12), ), ), - Divider(color: AppColors.of(context).inputFill.darken()), + Divider( + color: Theme.of(context) + .colorSchemeExtended + .inputFill + .darken()), Tappable( onTap: () => showColorPickerModal( context, @@ -77,7 +81,7 @@ class IconAndColorSelector extends StatelessWidget { onDataChange((color: selColor, icon: data.icon)); }), - bgColor: AppColors.of(context).inputFill, + bgColor: Theme.of(context).colorSchemeExtended.inputFill, child: ListTile( mouseCursor: SystemMouseCursors.click, title: Text(t.icon_selector.color), diff --git a/lib/app/categories/selectors/category_multi_selector.dart b/lib/app/categories/selectors/category_multi_selector.dart index 8e612687..93b51ccd 100644 --- a/lib/app/categories/selectors/category_multi_selector.dart +++ b/lib/app/categories/selectors/category_multi_selector.dart @@ -246,7 +246,7 @@ class _CategoryMultiSelectorModalState }, ), ScrollableWithBottomGradient.buildPositionedGradient( - AppColors.of(context).modalBackground), + Theme.of(context).colorSchemeExtended.modalBackground), ], ), ); diff --git a/lib/app/categories/selectors/category_picker.dart b/lib/app/categories/selectors/category_picker.dart index 799f534e..bca16092 100644 --- a/lib/app/categories/selectors/category_picker.dart +++ b/lib/app/categories/selectors/category_picker.dart @@ -144,7 +144,9 @@ class _CategoryPickerState extends State { // buildSelectAllButton(snapshot), Expanded( child: ScrollableWithBottomGradient( - gradientColor: AppColors.of(context).modalBackground, + gradientColor: Theme.of(context) + .colorSchemeExtended + .modalBackground, padding: const EdgeInsets.symmetric(vertical: 16), controller: scrollController, child: buildCategoryList(snapshot, scrollController), @@ -241,7 +243,7 @@ class _CategoryPickerState extends State { style: TextStyle( color: selectedCategory?.id == subcat.id ? Colors.white - : AppColors.of(context).onSurface, + : Theme.of(context).colorScheme.onSurface, ), ), showCheckmark: false, diff --git a/lib/app/currencies/exchange_rate_form.dart b/lib/app/currencies/exchange_rate_form.dart index 0e261c8a..2c03de9d 100644 --- a/lib/app/currencies/exchange_rate_form.dart +++ b/lib/app/currencies/exchange_rate_form.dart @@ -165,7 +165,10 @@ class _ExchangeRateFormDialogState extends State { child: Icon( Icons.circle, size: 25, - color: AppColors.of(context).inputFill.darken(0.2), + color: Theme.of(context) + .colorSchemeExtended + .inputFill + .darken(0.2), ), ), ), diff --git a/lib/app/home/dashboard.page.dart b/lib/app/home/dashboard.page.dart index 984e23fb..e6174aea 100644 --- a/lib/app/home/dashboard.page.dart +++ b/lib/app/home/dashboard.page.dart @@ -4,19 +4,22 @@ import 'package:monekin/app/accounts/account_form.dart'; import 'package:monekin/app/accounts/details/account_details.dart'; import 'package:monekin/app/home/widgets/click_tracker.dart'; import 'package:monekin/app/home/widgets/home_drawer.dart'; +import 'package:monekin/app/home/widgets/horizontal_scrollable_account_list.dart'; import 'package:monekin/app/home/widgets/income_or_expense_card.dart'; import 'package:monekin/app/home/widgets/new_transaction_fl_button.dart'; import 'package:monekin/app/settings/edit_profile_modal.dart'; import 'package:monekin/app/stats/stats_page.dart'; -import 'package:monekin/app/stats/widgets/balance_bar_chart_small.dart'; +import 'package:monekin/app/stats/widgets/balance_bar_chart.dart'; import 'package:monekin/app/stats/widgets/finance_health/finance_health_main_info.dart'; import 'package:monekin/app/stats/widgets/fund_evolution_line_chart.dart'; import 'package:monekin/app/stats/widgets/movements_distribution/chart_by_categories.dart'; import 'package:monekin/core/database/services/account/account_service.dart'; import 'package:monekin/core/database/services/user-setting/private_mode_service.dart'; import 'package:monekin/core/database/services/user-setting/user_setting_service.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; import 'package:monekin/core/models/account/account.dart'; import 'package:monekin/core/models/date-utils/date_period_state.dart'; +import 'package:monekin/core/presentation/animations/animated_expanded.dart'; import 'package:monekin/core/presentation/responsive/breakpoints.dart'; import 'package:monekin/core/presentation/responsive/responsive_row_column.dart'; import 'package:monekin/core/presentation/widgets/card_with_header.dart'; @@ -44,23 +47,42 @@ class DashboardPage extends StatefulWidget { class _DashboardPageState extends State { DatePeriodState dateRangeService = const DatePeriodState(); + final ScrollController _scrollController = ScrollController(); + bool showSmallHeader = false; @override void initState() { super.initState(); + + _scrollController.addListener(_setSmallHeaderVisible); } @override - Widget build(BuildContext context) { - final t = Translations.of(context); + void dispose() { + _scrollController.removeListener(_setSmallHeaderVisible); + _scrollController.dispose(); + super.dispose(); + } + void _setSmallHeaderVisible() { + final shouldShowSmallHeader = _scrollController.position.pixels > 150; + if (showSmallHeader != shouldShowSmallHeader) { + setState(() { + showSmallHeader = shouldShowSmallHeader; + }); + } + } + + @override + Widget build(BuildContext context) { final accountService = AccountService.instance; final hideDrawerAndFloatingButton = BreakPoint.of(context).isLargerOrEqualTo(BreakpointID.md); return Scaffold( - appBar: EmptyAppBar(color: AppColors.of(context).light), + appBar: EmptyAppBar( + color: Theme.of(context).colorSchemeExtended.dashboardHeader), floatingActionButton: hideDrawerAndFloatingButton ? null : const NewTransactionButton(), drawer: hideDrawerAndFloatingButton @@ -81,263 +103,258 @@ class _DashboardPageState extends State { ); }), ), - body: SingleChildScrollView( - child: Column(children: [ - DefaultTextStyle.merge( - style: - TextStyle(color: Theme.of(context).appBarTheme.foregroundColor), - child: Card( - margin: const EdgeInsets.only(bottom: 24), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Tappable( - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) { - return const EditProfileModal(); - }); - }, - bgColor: Colors.transparent, - borderRadius: 12, - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - if (BreakPoint.of(context) - .isSmallerThan(BreakpointID.md)) ...[ - StreamBuilder( - stream: UserSettingService.instance - .getSetting(SettingKey.avatar), - builder: (context, snapshot) { - return UserAvatar( - avatar: snapshot.data); - }), - const SizedBox(width: 8), - ], - Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: Stack( + children: [ + SingleChildScrollView( + controller: _scrollController, + child: Column(children: [ + Card( + color: Theme.of(context).colorSchemeExtended.dashboardHeader, + margin: const EdgeInsets.only(bottom: 24), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + buildWelcomeMsgAndAvatar(context), + buildDatePeriodSelector(context), + ], + ), + Divider( + height: 16, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + const SizedBox(height: 8), + StreamBuilder( + stream: AccountService.instance.getAccounts(), + builder: (context, accounts) { + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - Text( - "Welcome again!", - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - fontWeight: FontWeight.w300, - ), + totalBalanceIndicator( + context, accounts, accountService), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + IncomeOrExpenseCard( + type: TransactionType.E, + startDate: dateRangeService.startDate, + endDate: dateRangeService.endDate, + ), + IncomeOrExpenseCard( + type: TransactionType.I, + startDate: dateRangeService.startDate, + endDate: dateRangeService.endDate, + ), + ], ), - StreamBuilder( - stream: UserSettingService.instance - .getSetting(SettingKey.userName), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Skeleton( - width: 70, height: 12); - } - - return Text( - snapshot.data!, - style: Theme.of(context) - .textTheme - .titleSmall! - .copyWith( - fontWeight: FontWeight.w600, - fontSize: 18, - ), - ); - }), ], - ) - ], - ), - ), - ), - ActionChip( - label: Text(dateRangeService.getText(context)), - backgroundColor: - AppColors.of(context).primaryContainer, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - side: BorderSide( - style: BorderStyle.none, - color: AppColors.of(context).onPrimary, - ), - ), - onPressed: () { - openDatePeriodModal( - context, - DatePeriodModal( - initialDatePeriod: dateRangeService.datePeriod, - ), - ).then((value) { - if (value == null) return; - - setState(() { - dateRangeService = dateRangeService.copyWith( - periodModifier: 0, - datePeriod: value, ); - }); - }); - }, - ), - ], - ), - const Divider(height: 16), - const SizedBox(height: 8), - StreamBuilder( - stream: AccountService.instance.getAccounts(), - builder: (context, accounts) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - totalBalanceIndicator( - context, accounts, accountService), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IncomeOrExpenseCard( - type: TransactionType.I, - startDate: dateRangeService.startDate, - endDate: dateRangeService.endDate, - ), - IncomeOrExpenseCard( - type: TransactionType.E, - startDate: dateRangeService.startDate, - endDate: dateRangeService.endDate, - ), - ], - ), - ], - ); - }, - ), - ], + }) + ]), + ), ), - ), - ), - ), - _HorizontalScrollableAccountList( - dateRangeService: dateRangeService, - ), + HorizontalScrollableAccountList( + dateRangeService: dateRangeService, + ), - // ------------- STATS GENERAL CARDS -------------- + // ------------- STATS GENERAL CARDS -------------- - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24), - child: ResponsiveRowColumn.withSymetricSpacing( - direction: BreakPoint.of(context).isLargerThan(BreakpointID.md) - ? Axis.horizontal - : Axis.vertical, - rowCrossAxisAlignment: CrossAxisAlignment.start, - spacing: 16, - children: [ - ResponsiveRowColumnItem( - rowFit: FlexFit.tight, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CardWithHeader( - title: t.financial_health.display, - onHeaderButtonClick: () => RouteUtils.pushRoute( - context, - StatsPage( - dateRangeService: dateRangeService, - initialIndex: 0)), - bodyPadding: const EdgeInsets.all(16), - body: StreamBuilder( - stream: FinanceHealthService().getHealthyValue( - filters: TransactionFilters( - minDate: dateRangeService.startDate, - maxDate: dateRangeService.endDate, - ), - ), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const LinearProgressIndicator(); - } + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 20, + bottom: 64, + ), + child: DashboardCards(dateRangeService: dateRangeService), + ), + ]), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: AnimatedExpanded( + expand: showSmallHeader, + child: buildSmallHeader(context), + ), + ) + ], + )); + } + + ActionChip buildDatePeriodSelector(BuildContext context) { + return ActionChip( + label: Text(dateRangeService.getText(context), + style: TextStyle( + color: Theme.of(context).colorSchemeExtended.onDashboardHeader)), + backgroundColor: Theme.of(context).colorSchemeExtended.dashboardHeader, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + side: BorderSide( + // style: BorderStyle.none, + color: Theme.of(context).colorSchemeExtended.onDashboardHeader, + ), + ), + onPressed: () { + openDatePeriodModal( + context, + DatePeriodModal( + initialDatePeriod: dateRangeService.datePeriod, + ), + ).then((value) { + if (value == null) return; - final financeHealthData = snapshot.data!; + setState(() { + dateRangeService = dateRangeService.copyWith( + periodModifier: 0, + datePeriod: value, + ); + }); + }); + }, + ); + } - return FinanceHealthMainInfo( - financeHealthData: financeHealthData); - }, - ), + Tappable buildWelcomeMsgAndAvatar(BuildContext context) { + return Tappable( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (context) { + return const EditProfileModal(); + }); + }, + bgColor: Colors.transparent, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 8, 24, 8), + child: Row( + children: [ + if (BreakPoint.of(context).isSmallerThan(BreakpointID.md)) ...[ + StreamBuilder( + stream: + UserSettingService.instance.getSetting(SettingKey.avatar), + builder: (context, snapshot) { + return UserAvatar( + avatar: snapshot.data, + backgroundColor: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader + .darken(0.25), + border: Border.all( + width: 2, + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader, ), - const SizedBox(height: 16), - CardWithHeader( - title: t.stats.by_categories, - body: ChartByCategories( - datePeriodState: dateRangeService), - onHeaderButtonClick: () { - RouteUtils.pushRoute( - context, - StatsPage( - dateRangeService: dateRangeService, - initialIndex: 1), - ); - }), - ], - ), + ); + }), + const SizedBox(width: 12), + ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Welcome again!", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w300, + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader), ), - ResponsiveRowColumnItem( - rowFit: FlexFit.tight, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CardWithHeader( - title: t.stats.balance_evolution, - body: FundEvolutionLineChart( - dateRange: dateRangeService, - ), - onHeaderButtonClick: () { - RouteUtils.pushRoute( - context, - StatsPage( - dateRangeService: dateRangeService, - initialIndex: 2), - ); - }), - const SizedBox(height: 16), - CardWithHeader( - title: t.stats.cash_flow, - body: Padding( - padding: const EdgeInsets.only( - top: 16, left: 16, right: 16), - child: BalanceChartSmall( - dateRangeService: dateRangeService), - ), - onHeaderButtonClick: () { - RouteUtils.pushRoute( - context, - StatsPage( - dateRangeService: dateRangeService, - initialIndex: 3), - ); - }), - ], - ), - ) + StreamBuilder( + stream: UserSettingService.instance + .getSetting(SettingKey.userName), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Skeleton(width: 70, height: 12); + } + + return Text( + snapshot.data!, + style: Theme.of(context).textTheme.titleSmall!.copyWith( + fontWeight: FontWeight.w600, + fontSize: 18, + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader), + ); + }), + const SizedBox(width: 8), ], - ), + ) + ], + ), + ), + ); + } + + Widget buildSmallHeader( + BuildContext context, + ) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + color: Theme.of(context).colorSchemeExtended.dashboardHeader), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + t.home.total_balance, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader), + ), + StreamBuilder( + stream: AccountService.instance.getAccountsMoney(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return CurrencyDisplayer( + amountToConvert: snapshot.data!, + integerStyle: TextStyle( + fontSize: 26, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader), + ); + } + + return const Skeleton(width: 90, height: 40); + }, + ), + ], ), - ]))); + buildDatePeriodSelector(context) + ], + ), + ); } Widget totalBalanceIndicator( @@ -372,7 +389,8 @@ class _DashboardPageState extends State { children: [ Text( t.home.total_balance, - style: Theme.of(context).textTheme.labelSmall!, + style: Theme.of(context).textTheme.labelSmall!.copyWith( + color: Theme.of(context).colorSchemeExtended.onDashboardHeader), ), if (!accounts.hasData) ...[ const Skeleton(width: 70, height: 40), @@ -380,16 +398,17 @@ class _DashboardPageState extends State { ], if (accounts.hasData) ...[ StreamBuilder( - stream: accountService.getAccountsMoney( - accountIds: accounts.data!.map((e) => e.id)), + stream: AccountService.instance.getAccountsMoney(), builder: (context, snapshot) { if (snapshot.hasData) { return CurrencyDisplayer( amountToConvert: snapshot.data!, - integerStyle: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ), + integerStyle: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader), ); } @@ -412,6 +431,8 @@ class _DashboardPageState extends State { return TrendingValue( percentage: snapshot.data!, fontWeight: FontWeight.bold, + filled: true, + outlined: true, fontSize: 16, ); }, @@ -511,8 +532,9 @@ class _DashboardPageState extends State { } } -class _HorizontalScrollableAccountList extends StatelessWidget { - const _HorizontalScrollableAccountList({ +class DashboardCards extends StatelessWidget { + const DashboardCards({ + super.key, required this.dateRangeService, }); @@ -522,161 +544,103 @@ class _HorizontalScrollableAccountList extends StatelessWidget { Widget build(BuildContext context) { final t = Translations.of(context); - return Align( - alignment: Alignment.centerLeft, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamBuilder( - stream: AccountService.instance.getAccounts( - predicate: (acc, curr) => acc.closingDate.isNull(), - ), - builder: (context, snapshot) { - return Row( - children: [ - ...List.generate(snapshot.data?.length ?? 0, (index) { - final account = snapshot.data!.elementAt(index); - - return Card( - margin: const EdgeInsets.only(right: 8), - color: Colors.transparent, - elevation: 0, - child: Tappable( - onTap: () => RouteUtils.pushRoute( - context, - AccountDetailsPage( - account: account, - accountIconHeroTag: - 'dashboard-page__account-icon-${account.id}', - ), - ), - bgColor: AppColors.of(context).light, - borderRadius: 12, - child: Padding( - padding: const EdgeInsets.all(16), - child: SizedBox( - width: 250, - child: Column( - children: [ - Row(children: [ - Hero( - tag: - 'dashboard-page__account-icon-${account.id}', - child: account.displayIcon( - context, - size: 28, - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - account.name, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontWeight: FontWeight.bold, - ), - ), - Text( - account.type.title(context), - style: Theme.of(context) - .textTheme - .labelMedium!, - ) - ], - ) - ]), - const Divider(height: 24), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - StreamBuilder( - initialData: 0.0, - stream: AccountService.instance - .getAccountMoney(account: account), - builder: (context, snapshot) { - return CurrencyDisplayer( - amountToConvert: snapshot.data!, - currency: account.currency, - integerStyle: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( - fontWeight: FontWeight.w600, - ), - ); - }), - StreamBuilder( - initialData: 0.0, - stream: AccountService.instance - .getAccountsMoneyVariation( - accounts: [account], - startDate: dateRangeService.startDate, - endDate: dateRangeService.endDate, - convertToPreferredCurrency: false, - ), - builder: (context, snapshot) { - return TrendingValue( - percentage: snapshot.data!, - decimalDigits: 0, - ); - }), - ], - ), - ], - ), - ), - ), + return ResponsiveRowColumn.withSymetricSpacing( + direction: BreakPoint.of(context).isLargerThan(BreakpointID.md) + ? Axis.horizontal + : Axis.vertical, + rowCrossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + ResponsiveRowColumnItem( + rowFit: FlexFit.tight, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CardWithHeader( + title: t.financial_health.display, + footer: CardFooterWithSingleButton( + onButtonClick: () => RouteUtils.pushRoute( + context, + StatsPage( + dateRangeService: dateRangeService, initialIndex: 0), + ), + ), + bodyPadding: const EdgeInsets.all(16), + body: StreamBuilder( + stream: FinanceHealthService().getHealthyValue( + filters: TransactionFilters( + minDate: dateRangeService.startDate, + maxDate: dateRangeService.endDate, ), + ), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const LinearProgressIndicator(); + } + + final financeHealthData = snapshot.data!; + + return FinanceHealthMainInfo( + financeHealthData: financeHealthData); + }, + ), + ), + const SizedBox(height: 16), + CardWithHeader( + title: t.stats.by_categories, + body: ChartByCategories(datePeriodState: dateRangeService), + footer: CardFooterWithSingleButton( + onButtonClick: () => RouteUtils.pushRoute( + context, + StatsPage( + dateRangeService: dateRangeService, initialIndex: 1), + ), + ), + ), + ], + ), + ), + ResponsiveRowColumnItem( + rowFit: FlexFit.tight, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CardWithHeader( + title: t.stats.balance_evolution, + bodyPadding: const EdgeInsets.all(16), + body: FundEvolutionLineChart(dateRange: dateRangeService), + footer: CardFooterWithSingleButton(onButtonClick: () { + RouteUtils.pushRoute( + context, + StatsPage( + dateRangeService: dateRangeService, initialIndex: 2), ); }), - Opacity( - opacity: 0.6, - child: Tappable( - // bgColor: AppColors.of(context).light, - onTap: () { - RouteUtils.pushRoute(context, const AccountFormPage()); - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - width: 2, - color: Theme.of(context).dividerColor, - ), - ), - child: Card( - elevation: 0, - color: Colors.transparent, - margin: const EdgeInsets.all(0), - child: Padding( - padding: const EdgeInsets.all(16), - child: SizedBox( - width: 200, - height: 127.3 - 32 - 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(t.account.form.create), - const SizedBox(height: 8), - const Icon(Icons.add), - ], - ), - ), - ), - ), + ), + const SizedBox(height: 16), + CardWithHeader( + title: t.stats.by_periods, + bodyPadding: + const EdgeInsets.only(bottom: 12, top: 24, right: 16), + body: BalanceBarChart( + dateRange: dateRangeService, + filters: TransactionFilters( + minDate: dateRangeService.startDate, + maxDate: dateRangeService.endDate, ), - ) - ], - ); - }, - ), - ), + ), + footer: CardFooterWithSingleButton(onButtonClick: () { + RouteUtils.pushRoute( + context, + StatsPage( + dateRangeService: dateRangeService, initialIndex: 3), + ); + }), + ) + ], + ), + ) + ], ); } } diff --git a/lib/app/home/widgets/home_drawer.dart b/lib/app/home/widgets/home_drawer.dart index 4eba5f0c..bb68e6e8 100644 --- a/lib/app/home/widgets/home_drawer.dart +++ b/lib/app/home/widgets/home_drawer.dart @@ -71,7 +71,7 @@ class HomeDrawer extends StatelessWidget { return UserAccountsDrawerHeader( decoration: BoxDecoration( - color: AppColors.of(context).surface, + color: Theme.of(context).colorScheme.surface, ), accountName: UserGreting(userName: userName), currentAccountPicture: UserAvatar(avatar: userAvatar), @@ -107,7 +107,7 @@ class UserGreting extends StatelessWidget { style: TextStyle( fontWeight: FontWeight.w300, fontSize: 12, - color: AppColors.of(context).onSurface, + color: Theme.of(context).colorScheme.onSurface, ), ), if (userName == null) @@ -116,7 +116,7 @@ class UserGreting extends StatelessWidget { Text( userName!, style: TextStyle( - color: AppColors.of(context).onSurface, + color: Theme.of(context).colorScheme.onSurface, ), ), ], diff --git a/lib/app/home/widgets/horizontal_scrollable_account_list.dart b/lib/app/home/widgets/horizontal_scrollable_account_list.dart new file mode 100644 index 00000000..25b41d8b --- /dev/null +++ b/lib/app/home/widgets/horizontal_scrollable_account_list.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:monekin/app/accounts/account_form.dart'; +import 'package:monekin/app/accounts/details/account_details.dart'; +import 'package:monekin/core/database/services/account/account_service.dart'; +import 'package:monekin/core/models/date-utils/date_period_state.dart'; +import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_displayer.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; +import 'package:monekin/core/presentation/widgets/trending_value.dart'; +import 'package:monekin/core/routes/route_utils.dart'; +import 'package:monekin/i18n/translations.g.dart'; + +class HorizontalScrollableAccountList extends StatelessWidget { + const HorizontalScrollableAccountList({ + required this.dateRangeService, + }); + + final DatePeriodState dateRangeService; + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + return Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: StreamBuilder( + stream: AccountService.instance.getAccounts( + predicate: (acc, curr) => acc.closingDate.isNull(), + ), + builder: (context, snapshot) { + return Row( + children: [ + ...List.generate(snapshot.data?.length ?? 0, (index) { + final account = snapshot.data!.elementAt(index); + + return Card( + margin: const EdgeInsets.only(right: 8), + color: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9999), + ), + child: Tappable( + onTap: () => RouteUtils.pushRoute( + context, + AccountDetailsPage( + account: account, + accountIconHeroTag: + 'dashboard-page__account-icon-${account.id}', + ), + ), + bgColor: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(9999), + child: Padding( + padding: const EdgeInsets.all(16), + child: SizedBox( + width: 250, + child: Row( + children: [ + Hero( + tag: + 'dashboard-page__account-icon-${account.id}', + child: account.displayIcon( + context, + size: 28, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + account.name, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + StreamBuilder( + initialData: 0.0, + stream: AccountService.instance + .getAccountMoney( + account: account), + builder: (context, snapshot) { + return CurrencyDisplayer( + amountToConvert: snapshot.data!, + currency: account.currency, + integerStyle: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontWeight: FontWeight.w600, + ), + ); + }), + const SizedBox(width: 8), + StreamBuilder( + initialData: 0.0, + stream: AccountService.instance + .getAccountsMoneyVariation( + accounts: [account], + startDate: + dateRangeService.startDate, + endDate: dateRangeService.endDate, + convertToPreferredCurrency: false, + ), + builder: (context, snapshot) { + return TrendingValue( + percentage: snapshot.data!, + decimalDigits: 0, + ); + }), + ], + ) + ], + ), + ], + ), + ), + ), + ), + ); + }), + // Add account card + Opacity( + opacity: 0.6, + child: Tappable( + onTap: () { + RouteUtils.pushRoute(context, const AccountFormPage()); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(99999), + side: BorderSide( + width: 2, + color: Theme.of(context).dividerColor, + ), + ), + child: Card( + elevation: 0, + color: Colors.transparent, + margin: const EdgeInsets.all(0), + child: SizedBox( + width: 200, + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(t.account.form.create), + const SizedBox(height: 4), + const Icon(Icons.add), + ], + ), + ), + ), + ), + ) + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/app/home/widgets/income_or_expense_card.dart b/lib/app/home/widgets/income_or_expense_card.dart index 4792c552..c4576b82 100644 --- a/lib/app/home/widgets/income_or_expense_card.dart +++ b/lib/app/home/widgets/income_or_expense_card.dart @@ -35,7 +35,7 @@ class IncomeOrExpenseCard extends StatelessWidget { ), child: Icon( type.icon, - color: AppColors.of(context).surface, + color: Theme.of(context).colorScheme.surface, size: 22, ), ), @@ -43,7 +43,13 @@ class IncomeOrExpenseCard extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(type.displayName(context)), + Text( + type.displayName(context), + style: TextStyle( + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader), + ), StreamBuilder( stream: AccountService.instance.getAccountsBalance( filters: TransactionFilters( @@ -61,7 +67,11 @@ class IncomeOrExpenseCard extends StatelessWidget { return CurrencyDisplayer( amountToConvert: snapshot.data!.abs(), - integerStyle: const TextStyle(fontSize: 18), + integerStyle: TextStyle( + fontSize: 18, + color: Theme.of(context) + .colorSchemeExtended + .onDashboardHeader), ); }) ], diff --git a/lib/app/layout/tabs.dart b/lib/app/layout/tabs.dart index cb0d95ec..45cb6fb7 100644 --- a/lib/app/layout/tabs.dart +++ b/lib/app/layout/tabs.dart @@ -42,8 +42,12 @@ class TabsPageState extends State { selectedNavItemIndex < menuItems.length) ? null : NavigationBar( + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerHigh, + indicatorColor: + Theme.of(context).colorScheme.primary.withOpacity(0.2), destinations: menuItems - .map((e) => e.toNavigationDestinationWidget()) + .map((e) => e.toNavigationDestinationWidget(context)) .toList(), selectedIndex: selectedNavItemIndex, onDestinationSelected: (e) => changePage(menuItems.elementAt(e)), diff --git a/lib/app/onboarding/intro.page.dart b/lib/app/onboarding/intro.page.dart index 44b6c2b4..7752e5d9 100644 --- a/lib/app/onboarding/intro.page.dart +++ b/lib/app/onboarding/intro.page.dart @@ -39,7 +39,7 @@ class IntroPage extends StatelessWidget { style: Theme.of(context) .textTheme .labelSmall! - .copyWith(color: AppColors.of(context).primary), + .copyWith(color: Theme.of(context).colorScheme.primary), ), ], ); diff --git a/lib/app/onboarding/onboarding.dart b/lib/app/onboarding/onboarding.dart index 66716301..899c4c50 100644 --- a/lib/app/onboarding/onboarding.dart +++ b/lib/app/onboarding/onboarding.dart @@ -197,8 +197,8 @@ class _OnboardingPageState extends State { dotsDecorator: DotsDecorator( size: const Size.square(10.0), activeSize: const Size(20.0, 10.0), - activeColor: AppColors.of(context).primary, - color: AppColors.of(context).primary.withOpacity(0.3), + activeColor: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.primary.withOpacity(0.3), spacing: const EdgeInsets.symmetric(horizontal: 3.0), activeShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25.0)), diff --git a/lib/app/settings/appearance_settings_page.dart b/lib/app/settings/appearance_settings_page.dart index 0530754a..942c439a 100644 --- a/lib/app/settings/appearance_settings_page.dart +++ b/lib/app/settings/appearance_settings_page.dart @@ -218,7 +218,7 @@ class _AdvancedSettingsPageState extends State { late final Color color; if (snapshot.data! == 'auto') { - color = AppColors.of(context).primary; + color = Theme.of(context).colorScheme.primary; } else { color = ColorHex.get(snapshot.data!); } diff --git a/lib/app/settings/import_csv.dart b/lib/app/settings/import_csv.dart index 91b394c0..cf783e5b 100644 --- a/lib/app/settings/import_csv.dart +++ b/lib/app/settings/import_csv.dart @@ -99,7 +99,7 @@ class _ImportCSVPageState extends State { final t = Translations.of(context); icon ??= SupportedIconService.instance.defaultSupportedIcon; - iconColor ??= AppColors.of(context).primary; + iconColor ??= Theme.of(context).colorScheme.primary; return TextFormField( controller: diff --git a/lib/app/settings/widgets/setting_card_item.dart b/lib/app/settings/widgets/setting_card_item.dart index 83c0123c..1dfef4d3 100644 --- a/lib/app/settings/widgets/setting_card_item.dart +++ b/lib/app/settings/widgets/setting_card_item.dart @@ -27,15 +27,15 @@ class SettingCardItem extends StatelessWidget { return Tappable( bgColor: isPrimary ? isAppInLightBrightness(context) - ? AppColors.of(context).primary.lighten(0.8) - : AppColors.of(context).primary.darken(0.8) + ? Theme.of(context).colorScheme.primary.lighten(0.8) + : Theme.of(context).colorScheme.primary.darken(0.8) : null, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide( width: 2, color: isPrimary - ? AppColors.of(context).primary + ? Theme.of(context).colorScheme.primary : Theme.of(context).dividerColor, ), ), @@ -51,9 +51,9 @@ class SettingCardItem extends StatelessWidget { children: [ Icon( icon, - color: isPrimary ? AppColors.of(context).primary : null, + color: isPrimary ? Theme.of(context).colorScheme.primary : null, size: mainAxis == Axis.horizontal ? 24 : 28, - // color: AppColors.of(context).primary, + // color: Theme.of(context).colorScheme.primary, ), if (mainAxis == Axis.horizontal) const SizedBox(width: 12), if (mainAxis == Axis.vertical) const SizedBox(height: 8), diff --git a/lib/app/settings/widgets/settings_list_separator.dart b/lib/app/settings/widgets/settings_list_separator.dart index 34009fee..c23b55db 100644 --- a/lib/app/settings/widgets/settings_list_separator.dart +++ b/lib/app/settings/widgets/settings_list_separator.dart @@ -9,7 +9,7 @@ Widget createListSeparator(BuildContext context, String title) { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, - color: AppColors.of(context).primary), + color: Theme.of(context).colorScheme.primary), ), ); } diff --git a/lib/app/stats/stats_page.dart b/lib/app/stats/stats_page.dart index 1a48dd8b..2cc0bd05 100644 --- a/lib/app/stats/stats_page.dart +++ b/lib/app/stats/stats_page.dart @@ -162,6 +162,9 @@ class _StatsPageState extends State { buildContainerWithPadding([ CardWithHeader( title: t.stats.balance_evolution, + subtitle: t.stats.balance_evolution_subtitle, + bodyPadding: const EdgeInsets.only( + bottom: 12, top: 16, right: 16, left: 16), body: FundEvolutionLineChart( showBalanceHeader: true, dateRange: dateRangeService, @@ -177,6 +180,7 @@ class _StatsPageState extends State { buildContainerWithPadding([ CardWithHeader( title: t.stats.cash_flow, + subtitle: t.stats.cash_flow_subtitle, body: IncomeExpenseComparason( startDate: dateRangeService.startDate, endDate: dateRangeService.endDate, @@ -186,7 +190,8 @@ class _StatsPageState extends State { const SizedBox(height: 16), CardWithHeader( title: t.stats.by_periods, - bodyPadding: const EdgeInsets.only(bottom: 12, top: 16), + bodyPadding: + const EdgeInsets.only(bottom: 12, top: 24, right: 16), body: BalanceBarChart( dateRange: dateRangeService, filters: filters, diff --git a/lib/app/stats/widgets/balance_bar_chart.dart b/lib/app/stats/widgets/balance_bar_chart.dart index aa6662ff..bb199acc 100644 --- a/lib/app/stats/widgets/balance_bar_chart.dart +++ b/lib/app/stats/widgets/balance_bar_chart.dart @@ -306,11 +306,16 @@ class _BalanceBarChartState extends State { : Colors.white24; return BarChart(BarChartData( + maxY: snapshot.data!.expense.every((ex) => ex == 0) && + snapshot.data!.income.every((inc) => inc == 0) && + snapshot.data!.balance.every((bal) => bal == 0) + ? 10.2 + : null, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( tooltipMargin: -10, getTooltipColor: (spot) => - AppColors.of(context).surface, + Theme.of(context).colorScheme.surface, getTooltipItem: (group, groupIndex, rod, rodIndex) { final barRodsToY = group.barRods.map((e) => e.toY); @@ -387,50 +392,42 @@ class _BalanceBarChartState extends State { }, ), ), - rightTitles: AxisTitles( + leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { + if (value == meta.max) { + return Container(); + } + return SideTitleWidget( axisSide: meta.axisSide, - space: 0, - child: Row( - children: [ - Container( - width: 5, - height: 1, - color: ultraLightBorderColor, - ), - const SizedBox(width: 4), - BlurBasedOnPrivateMode( - child: Text( - meta.formattedValue, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.w300, - ), - ), + child: BlurBasedOnPrivateMode( + child: Text( + meta.formattedValue, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w300, ), - ], + ), ), ); }, reservedSize: 42, ), ), - leftTitles: const AxisTitles( + rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData( - show: true, - border: Border( - bottom: BorderSide( - width: 1, color: ultraLightBorderColor), - right: BorderSide( - width: 1, color: ultraLightBorderColor), - )), + show: true, + border: Border( + bottom: + BorderSide(width: 1, color: ultraLightBorderColor), + ), + ), gridData: FlGridData( drawVerticalLine: false, getDrawingHorizontalLine: (value) { diff --git a/lib/app/stats/widgets/balance_bar_chart_small.dart b/lib/app/stats/widgets/balance_bar_chart_small.dart deleted file mode 100644 index 03030730..00000000 --- a/lib/app/stats/widgets/balance_bar_chart_small.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'package:async/async.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:monekin/core/database/services/account/account_service.dart'; -import 'package:monekin/core/models/date-utils/date_period_state.dart'; -import 'package:monekin/core/presentation/responsive/breakpoints.dart'; -import 'package:monekin/core/presentation/theme.dart'; -import 'package:monekin/core/presentation/widgets/transaction_filter/transaction_filters.dart'; -import 'package:monekin/i18n/translations.g.dart'; - -import '../../../core/models/transaction/transaction_type.enum.dart'; -import '../../../core/presentation/app_colors.dart'; - -class BalanceChartSmall extends StatefulWidget { - const BalanceChartSmall({super.key, required this.dateRangeService}); - - final DatePeriodState dateRangeService; - - @override - State createState() => _BalanceChartSmallState(); -} - -class _BalanceChartSmallState extends State { - int touchedGroupIndex = -1; - - BarChartGroupData makeGroupData(int x, double expense, double income, - {bool disabled = false, required AppColors colors}) { - const double width = 56; - - const radius = BorderRadius.vertical( - bottom: Radius.zero, top: Radius.circular(width / 6)); - - return BarChartGroupData( - barsSpace: 4, - x: x, - barRods: [ - BarChartRodData( - toY: expense, - color: disabled - ? Colors.grey.withOpacity(0.175) - : x == 0 - ? colors.danger.withOpacity(0.4) - : colors.danger, - borderRadius: radius, - width: width, - ), - BarChartRodData( - toY: income, - color: disabled - ? Colors.grey.withOpacity(0.175) - : x == 0 - ? colors.success.withOpacity(0.4) - : colors.success, - borderRadius: radius, - width: width, - ), - ], - ); - } - - FlTitlesData getTitlesData(Color borderColor) { - return FlTitlesData( - show: true, - rightTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 32, - getTitlesWidget: (value, meta) { - return Row( - children: [ - Container( - width: 5, - height: 1, - color: borderColor, - ), - const SizedBox(width: 4), - Text( - meta.formattedValue, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.w300, - ), - ), - ], - ); - }, - )), - topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 24, - getTitlesWidget: (value, meta) { - return Text( - value == 0 ? 'Periodo anterior' : 'Este periodo', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w300, - ), - ); - }, - ), - ), - leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), - ); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - - return SizedBox( - height: BreakPoint.of(context).isLargerThan(BreakpointID.md) ? 325 : 250, - child: StreamBuilder( - stream: AccountService.instance.getAccounts(), - builder: (context, accountsSnapshot) { - if (!accountsSnapshot.hasData) { - return const CircularProgressIndicator(); - } - - final accounts = accountsSnapshot.data!; - - final ultraLightBorderColor = isAppInLightBrightness(context) - ? Colors.black12 - : Colors.white12; - - if (accounts.isEmpty) { - return Stack( - children: [ - BarChart( - BarChartData( - barTouchData: BarTouchData( - touchTooltipData: BarTouchTooltipData( - getTooltipItem: (a, b, c, d) => null, - ), - ), - titlesData: getTitlesData(ultraLightBorderColor), - borderData: FlBorderData( - show: true, - border: Border( - bottom: BorderSide( - width: 1, color: ultraLightBorderColor), - ), - ), - barGroups: [ - makeGroupData(0, 4, 2, - disabled: true, colors: AppColors.of(context)), - makeGroupData(1, 5, 7, - disabled: true, colors: AppColors.of(context)), - ], - gridData: const FlGridData(show: false), - ), - ), - Positioned.fill( - child: Align( - alignment: Alignment.center, - child: Text( - t.general.insufficient_data, - style: Theme.of(context).textTheme.titleLarge, - )), - ) - ], - ); - } - - return StreamBuilder( - stream: StreamZip([ - AccountService.instance.getAccountsBalance( - filters: TransactionFilters( - transactionTypes: [TransactionType.E], - minDate: widget.dateRangeService.getPrevDates().$1, - maxDate: widget.dateRangeService.getPrevDates().$2, - )), - AccountService.instance.getAccountsBalance( - filters: TransactionFilters( - transactionTypes: [TransactionType.I], - minDate: widget.dateRangeService.getPrevDates().$1, - maxDate: widget.dateRangeService.getPrevDates().$2, - )), - AccountService.instance.getAccountsBalance( - filters: TransactionFilters( - transactionTypes: [TransactionType.E], - minDate: widget.dateRangeService.startDate, - maxDate: widget.dateRangeService.endDate, - )), - AccountService.instance.getAccountsBalance( - filters: TransactionFilters( - transactionTypes: [TransactionType.I], - minDate: widget.dateRangeService.startDate, - maxDate: widget.dateRangeService.endDate, - )), - ]), - builder: (context, snapshpot) { - if (!snapshpot.hasData) { - return const CircularProgressIndicator(); - } - - return BarChart( - BarChartData( - barTouchData: BarTouchData( - touchTooltipData: BarTouchTooltipData( - getTooltipColor: (group) => - AppColors.of(context).surface, - getTooltipItem: (a, b, c, d) => null, - ), - ), - titlesData: getTitlesData(ultraLightBorderColor), - borderData: FlBorderData( - show: true, - border: Border( - bottom: BorderSide( - width: 1, color: ultraLightBorderColor), - right: BorderSide( - width: 1, color: ultraLightBorderColor), - ), - ), - barGroups: [ - makeGroupData( - 0, -snapshpot.data![0], snapshpot.data![1], - colors: AppColors.of(context)), - makeGroupData( - 1, -snapshpot.data![2], snapshpot.data![3], - colors: AppColors.of(context)), - ], - gridData: const FlGridData(show: false), - ), - ); - }); - }), - ); - } -} diff --git a/lib/app/stats/widgets/fund_evolution_line_chart.dart b/lib/app/stats/widgets/fund_evolution_line_chart.dart index 2dc85218..acba34bd 100644 --- a/lib/app/stats/widgets/fund_evolution_line_chart.dart +++ b/lib/app/stats/widgets/fund_evolution_line_chart.dart @@ -1,10 +1,10 @@ -import 'package:collection/collection.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:monekin/core/database/services/account/account_service.dart'; import 'package:monekin/core/database/services/currency/currency_service.dart'; import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/extensions/lists.extensions.dart'; import 'package:monekin/core/models/date-utils/date_period_state.dart'; import 'package:monekin/core/presentation/theme.dart'; import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_displayer.dart'; @@ -16,8 +16,6 @@ import 'package:monekin/core/utils/constants.dart'; import 'package:monekin/i18n/translations.g.dart'; import 'package:rxdart/rxdart.dart'; -import '../../../core/presentation/app_colors.dart'; - class LineChartDataItem { List balance; List labels; @@ -70,7 +68,7 @@ class FundEvolutionLineChart extends StatelessWidget { @override Widget build(BuildContext context) { - final lineColor = AppColors.of(context).primary; + final lineColor = Theme.of(context).colorScheme.primary; final accountService = AccountService.instance; @@ -78,102 +76,96 @@ class FundEvolutionLineChart extends StatelessWidget { return Column( children: [ - if (showBalanceHeader) ...[ + if (showBalanceHeader) StreamBuilder( - stream: filters.accounts(), - builder: (context, accountsSnapshot) { - if (!accountsSnapshot.hasData) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Final balance - ${dateRange.getText(context)}', - style: const TextStyle(fontSize: 12)), - const Skeleton(width: 70, height: 40), - const Skeleton(width: 30, height: 14), - ], - ); - } else { - final accounts = accountsSnapshot.data!; + stream: filters.accounts(), + builder: (context, accountsSnapshot) { + if (!accountsSnapshot.hasData) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Final balance - ${dateRange.getText(context)}', + style: const TextStyle(fontSize: 12)), + const Skeleton(width: 70, height: 40), + const Skeleton(width: 30, height: 14), + ], + ); + } else { + final accounts = accountsSnapshot.data!; - return Padding( - padding: const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(t.stats.final_balance, - style: const TextStyle(fontSize: 12)), - StreamBuilder( - stream: accountService.getAccountsMoney( - accountIds: accounts.map((e) => e.id), - trFilters: filters, - date: dateRange.endDate, - ), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Skeleton( - width: 70, height: 40); - } + Text(t.stats.final_balance, + style: const TextStyle(fontSize: 12)), + StreamBuilder( + stream: accountService.getAccountsMoney( + accountIds: accounts.map((e) => e.id), + trFilters: filters, + date: dateRange.endDate, + ), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Skeleton(width: 70, height: 40); + } - return CurrencyDisplayer( - amountToConvert: snapshot.data!, - integerStyle: Theme.of(context) - .textTheme - .headlineSmall!); - }), - ], + return CurrencyDisplayer( + amountToConvert: snapshot.data!, + integerStyle: Theme.of(context) + .textTheme + .headlineSmall!); + }), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + t.stats.compared_to_previous_period, + style: const TextStyle(fontSize: 12), ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - t.stats.compared_to_previous_period, - style: const TextStyle(fontSize: 12), + StreamBuilder( + stream: accountService.getAccountsMoneyVariation( + accounts: accounts, + startDate: dateRange.startDate, + endDate: dateRange.endDate, + convertToPreferredCurrency: true, ), - StreamBuilder( - stream: - accountService.getAccountsMoneyVariation( - accounts: accounts, - startDate: dateRange.startDate, - endDate: dateRange.endDate, - convertToPreferredCurrency: true, - ), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Skeleton( - width: 52, height: 22); - } + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Skeleton(width: 52, height: 22); + } - return TrendingValue( - percentage: snapshot.data!, - filled: false, - fontWeight: Theme.of(context) - .textTheme - .headlineSmall! - .fontWeight!, - fontSize: Theme.of(context) - .textTheme - .headlineSmall! - .fontSize!, - outlined: false, - ); - }) - ], - ) + return TrendingValue( + percentage: snapshot.data!, + filled: false, + fontWeight: Theme.of(context) + .textTheme + .headlineSmall! + .fontWeight!, + fontSize: Theme.of(context) + .textTheme + .headlineSmall! + .fontSize!, + outlined: false, + ); + }) ], - ), - ); - } - }), - const SizedBox(height: 16), - ], + ) + ], + ); + } + }, + ), + const SizedBox(height: 24), SizedBox( - height: 300, + height: 260, child: StreamBuilder( stream: getEvolutionData(), builder: (context, snapshot) { @@ -215,7 +207,7 @@ class FundEvolutionLineChart extends StatelessWidget { touchTooltipData: LineTouchTooltipData( tooltipMargin: -10, getTooltipColor: (spot) => - AppColors.of(context).surface, + Theme.of(context).colorScheme.surface, getTooltipItems: (touchedSpots) { return touchedSpots.map((barSpot) { final flSpot = barSpot; @@ -241,25 +233,27 @@ class FundEvolutionLineChart extends StatelessWidget { ), ), minY: snapshot.hasData - ? snapshot.data!.balance.min - - snapshot.data!.balance.min * 0.1 + ? (snapshot.data!.balance.allItemsEqual() + ? snapshot.data!.balance.first - 10.2 + : null) : 2, maxY: snapshot.hasData - ? snapshot.data!.balance.max + - snapshot.data!.balance.max * 0.1 + ? (snapshot.data!.balance.allItemsEqual() + ? snapshot.data!.balance.first + 10.2 + : null) : 5, titlesData: FlTitlesData( show: true, - leftTitles: const AxisTitles( + rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), bottomTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), - rightTitles: AxisTitles( + leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: snapshot.hasData, - reservedSize: 42, + reservedSize: 28, getTitlesWidget: (value, meta) { if (value == meta.max || value == meta.min) { @@ -268,25 +262,18 @@ class FundEvolutionLineChart extends StatelessWidget { return SideTitleWidget( axisSide: meta.axisSide, - space: 0, - child: Row( - children: [ - Container( - width: 5, - height: 1, - color: ultraLightBorderColor, - ), - const SizedBox(width: 4), - BlurBasedOnPrivateMode( - child: Text( - meta.formattedValue, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.w300, - ), - ), + child: BlurBasedOnPrivateMode( + child: Text( + meta.formattedValue, + maxLines: 1, + textAlign: TextAlign.end, + softWrap: false, + overflow: TextOverflow.visible, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w300, ), - ], + ), ), ); }, diff --git a/lib/app/stats/widgets/income_expense_comparason.dart b/lib/app/stats/widgets/income_expense_comparason.dart index 14e1c4a4..d3dd4112 100644 --- a/lib/app/stats/widgets/income_expense_comparason.dart +++ b/lib/app/stats/widgets/income_expense_comparason.dart @@ -32,7 +32,7 @@ class IncomeExpenseComparason extends StatelessWidget { return Column( children: [ Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.only(left: 16, right: 16, top: 12), child: Row(children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart b/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart index 7b0de61b..27448264 100644 --- a/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart +++ b/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart @@ -68,16 +68,9 @@ class _ChartByCategoriesState extends State { } Future>> getEvolutionData( - BuildContext context, - ) async { + BuildContext context, List transactions) async { final data = >[]; - final transactionService = TransactionService.instance; - - final transactions = await transactionService - .getTransactions(filters: _getTransactionFilters()) - .first; - for (final transaction in transactions) { final trValue = transaction.currentValueInPreferredCurrency * (transactionsType == TransactionType.E ? -1 : 1); @@ -221,8 +214,10 @@ class _ChartByCategoriesState extends State { Widget build(BuildContext context) { final t = Translations.of(context); - return FutureBuilder( - future: getEvolutionData(context), + return StreamBuilder( + stream: TransactionService.instance + .getTransactions(filters: _getTransactionFilters()) + .asyncMap((data) => getEvolutionData(context, data)), builder: (context, snapshot) { if (!snapshot.hasData) { return const LinearProgressIndicator(); diff --git a/lib/app/stats/widgets/movements_distribution/tags_stats.dart b/lib/app/stats/widgets/movements_distribution/tags_stats.dart index 2e1aa1fc..fdeaaa10 100644 --- a/lib/app/stats/widgets/movements_distribution/tags_stats.dart +++ b/lib/app/stats/widgets/movements_distribution/tags_stats.dart @@ -41,7 +41,8 @@ class TagStats extends StatelessWidget { } if (trSnapshot.data!.isEmpty) { - return Padding( + return Container( + alignment: Alignment.center, padding: const EdgeInsets.all(24), child: Text( t.general.insufficient_data, @@ -67,13 +68,15 @@ class TagStats extends StatelessWidget { tagsInfo.sort((a, b) => a.value.compareTo(b.value)); if (tags.isEmpty || tagsInfo.isEmpty) { - return Padding( + return Container( + alignment: Alignment.center, padding: const EdgeInsets.all(24), child: Text( - tags.isEmpty - ? t.tags.empty_list - : t.general.insufficient_data, - textAlign: TextAlign.center), + tags.isEmpty + ? t.tags.empty_list + : t.general.insufficient_data, + textAlign: TextAlign.center, + ), ); } diff --git a/lib/app/tags/tag_list.page.dart b/lib/app/tags/tag_list.page.dart index f7475743..010116a0 100644 --- a/lib/app/tags/tag_list.page.dart +++ b/lib/app/tags/tag_list.page.dart @@ -78,7 +78,7 @@ class _TagListPageState extends State { onTap: () => RouteUtils.pushRoute(context, TagFormPage(tag: tag)), bgColor: AppColors.of(context).light, margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - borderRadius: 12, + borderRadius: BorderRadius.circular(12), child: ListTile( trailing: tags.length > 1 ? ReorderableDragIcon( diff --git a/lib/app/tags/tags_selector.modal.dart b/lib/app/tags/tags_selector.modal.dart index 69325a88..24601e05 100644 --- a/lib/app/tags/tags_selector.modal.dart +++ b/lib/app/tags/tags_selector.modal.dart @@ -92,7 +92,7 @@ class _TagSelectorState extends State { snapSizes: const [0.65], builder: (context, scrollController) { return ModalContainer( - title: t.tags.select, + title: t.tags.select.title, titleBuilder: selectedTags.isEmpty ? null : (title) { @@ -177,7 +177,7 @@ class _TagSelectorState extends State { value: selectedTags.any((element) => element == null), secondary: Icon( Icons.label_off_rounded, - color: AppColors.of(context).primary, + color: Theme.of(context).colorScheme.primary, ), title: Text(t.tags.without_tags), onChanged: (newValue) { @@ -222,7 +222,7 @@ class _TagSelectorState extends State { separatorBuilder: (context, index) => const Divider(), ), ScrollableWithBottomGradient.buildPositionedGradient( - AppColors.of(context).modalBackground), + Theme.of(context).colorSchemeExtended.modalBackground), ], ), ); diff --git a/lib/app/transactions/form/dialogs/amount_selector.dart b/lib/app/transactions/form/dialogs/amount_selector.dart new file mode 100644 index 00000000..2ea6297a --- /dev/null +++ b/lib/app/transactions/form/dialogs/amount_selector.dart @@ -0,0 +1,587 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:monekin/app/transactions/form/dialogs/evaluate_expression.dart'; +import 'package:monekin/core/database/app_db.dart'; +import 'package:monekin/core/extensions/bool.extension.dart'; +import 'package:monekin/core/extensions/numbers.extensions.dart'; +import 'package:monekin/core/presentation/animations/animated_expanded.dart'; +import 'package:monekin/core/presentation/app_colors.dart'; +import 'package:monekin/core/presentation/widgets/modal_container.dart'; +import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_displayer.dart'; + +class AmountSelector extends StatefulWidget { + const AmountSelector({ + super.key, + required this.initialAmount, + this.currency, + this.onSubmit, + this.enableSignToggleButton = true, + required this.title, + }); + + final String title; + + final double initialAmount; + final CurrencyInDB? currency; + + /// Display a button to change the sign of the current value (when the calculator is not enabled) + final bool enableSignToggleButton; + + final void Function(double amount)? onSubmit; + + @override + State createState() => _AmountSelectorState(); +} + +class _AmountSelectorState extends State { + late String amountString; + + double get valueToNumber { + if (amountString.trim() == '') { + return 0; + } else if (amountString.trim() == '-' || amountString.trim() == '-0') { + return -0; + } + + return evaluateExpression(amountString).roundWithDecimals(2); + } + + final FocusNode _focusNode = FocusNode(); + late FocusAttachment _focusAttachment; + + bool calculatorMode = false; + + @override + void initState() { + super.initState(); + + amountString = _parseInitialAmount(widget.initialAmount); + + _focusAttachment = _focusNode.attach(context, onKeyEvent: (node, event) { + bool keyIsPressed = event.runtimeType == KeyDownEvent || + event.runtimeType == KeyRepeatEvent; + + if (!keyIsPressed) { + return KeyEventResult.handled; + } + + if ((event.logicalKey == LogicalKeyboardKey.browserBack || + event.logicalKey == LogicalKeyboardKey.goBack || + event.logicalKey == LogicalKeyboardKey.escape)) { + Navigator.pop(context); + } + + for (final (index, element) in [ + LogicalKeyboardKey.digit0, + LogicalKeyboardKey.digit1, + LogicalKeyboardKey.digit2, + LogicalKeyboardKey.digit3, + LogicalKeyboardKey.digit4, + LogicalKeyboardKey.digit5, + LogicalKeyboardKey.digit6, + LogicalKeyboardKey.digit7, + LogicalKeyboardKey.digit8, + LogicalKeyboardKey.digit9, + ].indexed) { + if (event.logicalKey == element) { + addToAmount(index.toStringAsFixed(0)); + break; + } + } + + for (final (index, element) in [ + LogicalKeyboardKey.numpad0, + LogicalKeyboardKey.numpad1, + LogicalKeyboardKey.numpad2, + LogicalKeyboardKey.numpad3, + LogicalKeyboardKey.numpad4, + LogicalKeyboardKey.numpad5, + LogicalKeyboardKey.numpad6, + LogicalKeyboardKey.numpad7, + LogicalKeyboardKey.numpad8, + LogicalKeyboardKey.numpad9, + ].indexed) { + if (event.logicalKey == element) { + addToAmount(index.toStringAsFixed(0)); + break; + } + } + + if (event.logicalKey == LogicalKeyboardKey.period) { + addToAmount('.'); + } else if (event.logicalKey == LogicalKeyboardKey.numpadDecimal) { + addToAmount('.'); + } else if (event.logicalKey == LogicalKeyboardKey.comma) { + addToAmount('.'); + } else if (event.logicalKey == LogicalKeyboardKey.backspace) { + removeLastCharFromAmount(); + } else if (event.logicalKey == LogicalKeyboardKey.delete) { + removeLastCharFromAmount(); + } else if (event.logicalKey == LogicalKeyboardKey.enter) { + submitAmount(); + } + + return KeyEventResult.handled; + }); + + _focusNode.requestFocus(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + String _parseInitialAmount(double amount) { + if (amount == 0) { + return amount.isNegative ? '-' : ''; + } + + String stringAmount = amount.toStringAsFixed(2); + if (!stringAmount.contains('.')) { + return stringAmount; + } + + int index = stringAmount.length - 1; + while (stringAmount[index] == '0') { + index--; + } + if (stringAmount[index] == '.') { + index--; + } + + return stringAmount.substring(0, index + 1); + } + + bool _currentNumberHasDecimal() { + final exprSplit = splitExprByNumbersAndOperator(amountString); + + if (exprSplit.isEmpty) { + return false; + } + + return exprSplit.last.contains('.'); + } + + toggleSign() { + if (amountString.startsWith('-')) { + amountString = amountString.substring(1, amountString.length); + } else { + amountString = '-$amountString'; + } + + HapticFeedback.mediumImpact(); + + setState(() {}); + } + + void addToAmount(String newText) { + if (newText == '.' && _currentNumberHasDecimal()) { + return; + } + + final newInputIsOperator = CalculatorOperator.isOperator(newText); + + setNewAmount(String newSelectedAmount) { + if (newInputIsOperator) { + HapticFeedback.mediumImpact(); + } else { + HapticFeedback.selectionClick(); + } + + setState(() { + amountString = newSelectedAmount; + }); + } + + if (newInputIsOperator && !calculatorMode) { + return; + } else if (valueToNumber != 0 && + double.tryParse(newText) != null && + _currentNumberHasDecimal() && + !CalculatorOperator.exprHasOperator(amountString)) { + final decimalPlaces = splitExprByNumbersAndOperator(amountString) + .last + .split('.') + .elementAtOrNull(1); + + if (decimalPlaces != null && decimalPlaces.length >= 2) { + return; + } + + // Pass + } + + if (amountString.isEmpty || + amountString == CalculatorOperator.subtract.symbol) { + if (newText == '0') { + return; + } else if (newText == '.') { + if (valueToNumber.isNegative) { + setNewAmount('-0.'); + } else { + setNewAmount('0.'); + } + } else if (newInputIsOperator) { + setNewAmount('0$newText'); + } else { + final sign = valueToNumber.isNegative ? '-' : ''; + + setNewAmount('$sign$newText'); + } + } else if (CalculatorOperator.exprEndsWithOperator(amountString)) { + if (newText == '.') { + setNewAmount('${amountString}0.'); + } else if (newInputIsOperator) { + // Replace last operator: + setNewAmount( + amountString.substring(0, amountString.length - 1) + newText); + } else { + setNewAmount(amountString + newText); + } + } else { + setNewAmount(amountString + newText); + } + } + + submitAmount() { + HapticFeedback.lightImpact(); + + if (widget.onSubmit != null) { + widget.onSubmit!(valueToNumber); + } + } + + void clearAmount() { + setState(() { + amountString = '0'; + HapticFeedback.lightImpact(); + }); + } + + void removeLastCharFromAmount() { + if (amountString.isEmpty || + amountString == CalculatorOperator.subtract.symbol) { + return; + } + + setState(() { + amountString = amountString.substring(0, amountString.length - 1); + HapticFeedback.lightImpact(); + }); + } + + toggleCalculatorMode() { + calculatorMode = !calculatorMode; + + if (calculatorMode == false) { + amountString = _parseInitialAmount(valueToNumber); + } + + HapticFeedback.mediumImpact(); + + setState(() {}); + } + + @override + Widget build(BuildContext context) { + _focusAttachment.reparent(); + + return ModalContainer( + title: widget.title, + bodyPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedExpanded( + expand: CalculatorOperator.exprHasOperator(amountString), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text(amountString), + ], + )), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Builder(builder: (context) { + const bigSizeStyle = TextStyle( + fontSize: 32, + fontWeight: FontWeight.w700, + ); + + return CurrencyDisplayer( + amountToConvert: valueToNumber, + currency: widget.currency, + decimalsStyle: TextStyle( + fontWeight: FontWeight.w200, + fontSize: 22, + color: amountString.contains('.') + ? null + : Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.3)), + integerStyle: bigSizeStyle, + currencyStyle: bigSizeStyle, + ); + }) + ], + ), + ], + ), + ), + const SizedBox(height: 12), + const Divider(), + Flexible( + child: Container( + // height: min(MediaQuery.of(context).size.width * 0.8, 300), + margin: const EdgeInsets.only(top: 16), + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + CalculatorButton( + onClick: () => addToAmount( + CalculatorOperator.multiply.symbol), + text: '×', + style: CalculatorButtonStyle.secondary, + flex: calculatorMode.toInt(), + ), + CalculatorButton( + onClick: () => addToAmount('1'), text: '1'), + CalculatorButton( + onClick: () => addToAmount('4'), text: '4'), + CalculatorButton( + onClick: () => addToAmount('7'), text: '7'), + CalculatorButton( + onClick: () => addToAmount('0'), text: '0'), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + CalculatorButton( + onClick: () => + addToAmount(CalculatorOperator.divide.symbol), + text: '÷', + style: CalculatorButtonStyle.secondary, + flex: calculatorMode.toInt(), + ), + CalculatorButton( + onClick: () => addToAmount('2'), text: '2'), + CalculatorButton( + onClick: () => addToAmount('5'), text: '5'), + CalculatorButton( + onClick: () => addToAmount('8'), text: '8'), + CalculatorButton( + disabled: _currentNumberHasDecimal(), + onClick: () => addToAmount('.'), + text: '.', + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + CalculatorButton( + onClick: () => addToAmount( + CalculatorOperator.subtract.symbol), + text: '-', + style: CalculatorButtonStyle.secondary, + flex: calculatorMode.toInt(), + ), + CalculatorButton( + onClick: () => addToAmount('3'), text: '3'), + CalculatorButton( + onClick: () => addToAmount('6'), text: '6'), + CalculatorButton( + onClick: () => addToAmount('9'), text: '9'), + CalculatorButton( + onClick: toggleCalculatorMode, + style: CalculatorButtonStyle.secondary, + icon: calculatorMode + ? Icons.fullscreen_exit_rounded + : Icons.calculate_rounded, + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + CalculatorButton( + onClick: () => + addToAmount(CalculatorOperator.add.symbol), + text: '+', + style: CalculatorButtonStyle.secondary, + flex: calculatorMode.toInt(), + ), + CalculatorButton( + onClick: removeLastCharFromAmount, + onLongPress: clearAmount, + style: CalculatorButtonStyle.secondary, + icon: Icons.backspace_outlined, + ), + CalculatorButton( + onClick: toggleSign, + style: CalculatorButtonStyle.secondary, + icon: Icons.exposure_rounded, + flex: calculatorMode || + !widget.enableSignToggleButton + ? 0 + : 1, + ), + CalculatorButton( + disabled: valueToNumber == 0 || + valueToNumber.isInfinite || + valueToNumber.isNaN, + onClick: submitAmount, + icon: Icons.check_rounded, + style: CalculatorButtonStyle.submit, + flex: calculatorMode || + !widget.enableSignToggleButton + ? 3 + : 2, + ), + ], + ), + ), + ], + )), + ) + ]), + ); + } +} + +enum CalculatorButtonStyle { submit, main, secondary } + +class CalculatorButton extends StatelessWidget { + final String? text; + final IconData? icon; + final int flex; + final VoidCallback? onClick; + final VoidCallback? onLongPress; + + final bool disabled; + + final CalculatorButtonStyle style; + + const CalculatorButton({ + super.key, + this.text, + this.icon, + required this.onClick, + this.onLongPress, + this.flex = 1, + this.disabled = false, + this.style = CalculatorButtonStyle.main, + }) : assert((text != null && icon == null) || (text == null && icon != null), + 'You must specify either text or icon, not both.'); + + @override + Widget build(BuildContext context) { + EdgeInsets padding = + const EdgeInsets.symmetric(vertical: 2.5, horizontal: 2.5); + if (MediaQuery.of(context).size.width >= 600) { + padding = const EdgeInsets.symmetric(vertical: 4, horizontal: 5); + } else if (MediaQuery.of(context).size.width >= 1024) { + padding = const EdgeInsets.symmetric(vertical: 4, horizontal: 32); + } + + return AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.fastEaseInToSlowEaseOut, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.fastEaseInToSlowEaseOut, + switchOutCurve: Curves.fastOutSlowIn, + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Container( + key: ValueKey((text ?? icon.toString()) + flex.toString()), + height: 65.0 * flex, + width: double.infinity, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + padding: padding, + child: elevatedButton(context), + ), + ), + ); + } + + ElevatedButton elevatedButton(BuildContext context) { + Color effectiveTextColor = Theme.of(context).colorScheme.onSurface; + Color effectiveBgColor = Theme.of(context).colorScheme.surface; + + if (style == CalculatorButtonStyle.submit) { + effectiveTextColor = Theme.of(context).colorScheme.onPrimary; + effectiveBgColor = Theme.of(context).colorScheme.primary; + } else if (style == CalculatorButtonStyle.secondary) { + effectiveTextColor = + Theme.of(context).colorScheme.onSurface.withOpacity(0.9); + effectiveBgColor = Theme.of(context).colorScheme.surfaceContainerHigh; + } + + if (icon == Icons.backspace_outlined) { + effectiveTextColor = AppColors.of(context).danger; + } + + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).brightness == Brightness.light + ? effectiveBgColor.withOpacity(0.975) + : effectiveBgColor.withOpacity(0.85), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + shadowColor: effectiveBgColor.withOpacity(0.85), + surfaceTintColor: effectiveBgColor.withOpacity(0.85), + foregroundColor: effectiveTextColor, + disabledForegroundColor: effectiveTextColor.withOpacity(0.3), + disabledBackgroundColor: effectiveBgColor.withOpacity(0.3), + elevation: 0, + padding: const EdgeInsets.all(0), + ), + onPressed: disabled ? null : onClick, + onLongPress: disabled ? null : onLongPress, + child: icon != null + ? Icon(icon, size: 26) + : Text( + text!, + softWrap: false, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} diff --git a/lib/app/transactions/form/dialogs/evaluate_expression.dart b/lib/app/transactions/form/dialogs/evaluate_expression.dart new file mode 100644 index 00000000..dfe53783 --- /dev/null +++ b/lib/app/transactions/form/dialogs/evaluate_expression.dart @@ -0,0 +1,169 @@ +// ignore_for_file: prefer_single_quotes + +enum CalculatorOperator { + add('+'), + subtract('-'), + multiply('X'), + divide('÷'); + + final String symbol; + + const CalculatorOperator(this.symbol); + + @override + String toString() { + return symbol; + } + + static CalculatorOperator? fromString(String symbol) { + switch (symbol) { + case '+': + return CalculatorOperator.add; + case '-': + return CalculatorOperator.subtract; + case '*' || 'x' || 'X': + return CalculatorOperator.multiply; + case '/' || '÷': + return CalculatorOperator.divide; + default: + return null; + } + } + + double apply(double a, double b) { + switch (this) { + case CalculatorOperator.add: + return a + b; + case CalculatorOperator.subtract: + return a - b; + case CalculatorOperator.multiply: + return a * b; + case CalculatorOperator.divide: + return a / b; + } + } + + static bool exprHasOperator(String expression) { + expression = expression.trim(); + + for (int i = 1; i < expression.length; i++) { + // Start by zero to avoid returning true when the first number is negative + final char = expression[i]; + if (CalculatorOperator.fromString(char) != null) { + return true; + } + } + + return false; + } + + static bool isOperator(String char) { + if (char.length > 1) { + throw ArgumentError("Character can not have this legth"); + } + + return CalculatorOperator.fromString(char) != null; + } + + static bool exprEndsWithOperator(String expression) { + if (expression.isEmpty) { + return false; + } + + final lastChar = expression[expression.length - 1]; + return CalculatorOperator.fromString(lastChar) != null; + } + + static Iterable getAllSymbols() { + return values.map((op) => op.symbol); + } +} + +List splitExprByNumbersAndOperator(String expression) { + final operators = CalculatorOperator.getAllSymbols().join("|\\"); + // ignore: prefer_interpolation_to_compose_strings + return RegExp(r'(\d+\.?\d*|\' + operators + ')') + .allMatches(expression) + .map((m) => m.group(0)!) + .toList(); +} + +double evaluateExpression(String expression) { + // Remove any whitespace from the input string + expression = expression.replaceAll(' ', ''); + + // Handle negative sign at the start of the expression + if (expression.startsWith('-')) { + expression = + '0$expression'; // Prepend 0 to allow for correct parsing, e.g., "-3+4" becomes "0-3+4" + } + + // Ignore trailing operators by removing them if present + while (expression.isNotEmpty && + CalculatorOperator.fromString(expression[expression.length - 1]) != + null) { + expression = expression.substring(0, expression.length - 1); + } + + if (expression.isEmpty) { + throw ArgumentError('Invalid expression: no numbers found.'); + } + + final tokens = splitExprByNumbersAndOperator(expression); + + List postfix = _infixToPostfix(tokens); + return _evaluatePostfix(postfix); +} + +List _infixToPostfix(List tokens) { + final precedence = { + CalculatorOperator.add: 1, + CalculatorOperator.subtract: 1, + CalculatorOperator.multiply: 2, + CalculatorOperator.divide: 2, + }; + final operators = []; + final output = []; + + for (int i = 0; i < tokens.length; i++) { + final token = tokens[i]; + final op = CalculatorOperator.fromString(token); + + if (double.tryParse(token) != null) { + // If the token is a number, add it to the output + output.add(token); + } else if (op != null) { + // While the top of the operator stack has the same or greater precedence + while (operators.isNotEmpty && + precedence[operators.last]! >= precedence[op]!) { + output.add(operators.removeLast().toString()); + } + // Push the current operator to the stack + operators.add(op); + } + } + + // Pop any remaining operators onto the output + while (operators.isNotEmpty) { + output.add(operators.removeLast().toString()); + } + + return output; +} + +double _evaluatePostfix(List postfix) { + final stack = []; + + for (final token in postfix) { + if (double.tryParse(token) != null) { + stack.add(double.parse(token)); + } else { + final b = stack.removeLast(); + final a = stack.removeLast(); + final op = CalculatorOperator.fromString(token)!; + stack.add(op.apply(a, b)); + } + } + + return stack.last; +} diff --git a/lib/app/transactions/form/dialogs/transaction_more_info.modal.dart b/lib/app/transactions/form/dialogs/transaction_more_info.modal.dart deleted file mode 100644 index aefdee72..00000000 --- a/lib/app/transactions/form/dialogs/transaction_more_info.modal.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:monekin/app/tags/tags_selector.modal.dart'; -import 'package:monekin/app/transactions/form/dialogs/transaction_value_in_destiny_modal.dart'; -import 'package:monekin/core/extensions/lists.extensions.dart'; -import 'package:monekin/core/models/account/account.dart'; -import 'package:monekin/core/models/tags/tag.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; -import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; -import 'package:monekin/core/presentation/widgets/count_indicator.dart'; -import 'package:monekin/core/presentation/widgets/modal_container.dart'; -import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_displayer.dart'; -import 'package:monekin/i18n/translations.g.dart'; - -part 'transaction_more_info.modal.g.dart'; - -@CopyWith() -class TransactionMoreInfo { - final String? note; - final List tags; - -// Specify only if we are on a transfer: - final num? valueInDestiny; - final Account? transferAccount; - - const TransactionMoreInfo({ - this.note, - this.tags = const [], - this.valueInDestiny, - this.transferAccount, - }); -} - -Future showTransactionMoreInfoModal( - BuildContext context, { - required TransactionMoreInfo data, -}) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) { - return _TransactionMoreInfoModal( - initialData: data, - ); - }); -} - -class _TransactionMoreInfoModal extends StatefulWidget { - const _TransactionMoreInfoModal({required this.initialData}); - - final TransactionMoreInfo initialData; - - @override - State<_TransactionMoreInfoModal> createState() => - _TransactionMoreInfoModalState(); -} - -class _TransactionMoreInfoModalState extends State<_TransactionMoreInfoModal> { - late TransactionMoreInfo moreInfoData; - - @override - void initState() { - super.initState(); - - moreInfoData = widget.initialData.copyWith(); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - - var listTiles = [ - ListTile( - leading: const Icon(Icons.location_on_outlined), - trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16), - title: const Text('Location'), - subtitle: const Text('Coming soon!'), - enabled: false, - onTap: () {}, - ), - const Divider(), - ListTile( - leading: Icon(Tag.icon), - trailing: CountIndicatorWithExpandArrow( - countToDisplay: moreInfoData.tags.length, - ), - title: Text(t.tags.display(n: 2)), - subtitle: Text( - moreInfoData.tags.isEmpty - ? t.transaction.form.no_tags - : moreInfoData.tags.map((t) => t.name).printFormatted(), - style: TextStyle( - fontStyle: - moreInfoData.tags.isEmpty ? FontStyle.italic : FontStyle.normal, - ), - ), - onTap: () { - showTagListModal(context, - modal: TagSelector( - allowEmptySubmit: true, - includeNullTag: false, - selectedTags: moreInfoData.tags, - )).then((value) { - if (value != null) { - setState(() { - moreInfoData = moreInfoData.copyWith( - tags: value.nonNulls.toList(), - ); - }); - } - }); - }, - ), - const Divider(), - if (moreInfoData.valueInDestiny != null) ...[ - ListTile( - leading: const Icon(Icons.start_rounded), - trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16), - title: Text(t.transfer.form.value_in_destiny.title), - subtitle: CurrencyDisplayer( - amountToConvert: moreInfoData.valueInDestiny!.toDouble(), - currency: moreInfoData.transferAccount?.currency, - ), - enabled: moreInfoData.transferAccount != null, - onTap: () { - showTransactionValueInDestinyModal( - context, - initialValue: moreInfoData.valueInDestiny!, - currency: moreInfoData.transferAccount! - .currency, // Can not be null due to the `enabled` property of the tile - ).then((value) { - if (value != null) { - setState(() { - moreInfoData = moreInfoData.copyWith(valueInDestiny: value); - }); - } - }); - }, - ), - const Divider(), - ] - ]; - - return DraggableScrollableSheet( - expand: false, - maxChildSize: 0.85, - minChildSize: 0.85, - initialChildSize: 0.85, - builder: (context, sc) { - return ModalContainer( - title: t.transaction.details, - body: Column( - children: [ - SingleChildScrollView( - controller: sc, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - TextFormField( - initialValue: widget.initialData.note, - onChanged: (value) { - setState(() { - moreInfoData = moreInfoData.copyWith(note: value); - }); - }, - maxLines: 3, - decoration: InputDecoration( - border: InputBorder.none, - fillColor: AppColors.of(context).modalBackground, - hoverColor: AppColors.of(context).modalBackground, - focusColor: AppColors.of(context).modalBackground, - hintText: t.transaction.form.description_info, - ), - ), - const Divider(), - ...listTiles - ], - ), - ), - ], - ), - footer: BottomSheetFooter( - onSaved: () => Navigator.pop(context, moreInfoData), - ), - ); - }, - ); - } -} diff --git a/lib/app/transactions/form/dialogs/transaction_more_info.modal.g.dart b/lib/app/transactions/form/dialogs/transaction_more_info.modal.g.dart deleted file mode 100644 index fadd7352..00000000 --- a/lib/app/transactions/form/dialogs/transaction_more_info.modal.g.dart +++ /dev/null @@ -1,111 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'transaction_more_info.modal.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$TransactionMoreInfoCWProxy { - TransactionMoreInfo note(String? note); - - TransactionMoreInfo tags(List tags); - - TransactionMoreInfo valueInDestiny(num? valueInDestiny); - - TransactionMoreInfo transferAccount(Account? transferAccount); - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TransactionMoreInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// TransactionMoreInfo(...).copyWith(id: 12, name: "My name") - /// ```` - TransactionMoreInfo call({ - String? note, - List? tags, - num? valueInDestiny, - Account? transferAccount, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTransactionMoreInfo.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTransactionMoreInfo.copyWith.fieldName(...)` -class _$TransactionMoreInfoCWProxyImpl implements _$TransactionMoreInfoCWProxy { - const _$TransactionMoreInfoCWProxyImpl(this._value); - - final TransactionMoreInfo _value; - - @override - TransactionMoreInfo note(String? note) => this(note: note); - - @override - TransactionMoreInfo tags(List tags) => this(tags: tags); - - @override - TransactionMoreInfo valueInDestiny(num? valueInDestiny) => - this(valueInDestiny: valueInDestiny); - - @override - TransactionMoreInfo transferAccount(Account? transferAccount) => - this(transferAccount: transferAccount); - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TransactionMoreInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// TransactionMoreInfo(...).copyWith(id: 12, name: "My name") - /// ```` - TransactionMoreInfo call({ - Object? note = const $CopyWithPlaceholder(), - Object? tags = const $CopyWithPlaceholder(), - Object? valueInDestiny = const $CopyWithPlaceholder(), - Object? transferAccount = const $CopyWithPlaceholder(), - }) { - return TransactionMoreInfo( - note: note == const $CopyWithPlaceholder() - ? _value.note - // ignore: cast_nullable_to_non_nullable - : note as String?, - tags: tags == const $CopyWithPlaceholder() || tags == null - ? _value.tags - // ignore: cast_nullable_to_non_nullable - : tags as List, - valueInDestiny: valueInDestiny == const $CopyWithPlaceholder() - ? _value.valueInDestiny - // ignore: cast_nullable_to_non_nullable - : valueInDestiny as num?, - transferAccount: transferAccount == const $CopyWithPlaceholder() - ? _value.transferAccount - // ignore: cast_nullable_to_non_nullable - : transferAccount as Account?, - ); - } -} - -extension $TransactionMoreInfoCopyWith on TransactionMoreInfo { - /// Returns a callable class that can be used as follows: `instanceOfTransactionMoreInfo.copyWith(...)` or like so:`instanceOfTransactionMoreInfo.copyWith.fieldName(...)`. - // ignore: library_private_types_in_public_api - _$TransactionMoreInfoCWProxy get copyWith => - _$TransactionMoreInfoCWProxyImpl(this); - - /// Copies the object with the specific fields set to `null`. If you pass `false` as a parameter, nothing will be done and it will be ignored. Don't do it. Prefer `copyWith(field: null)` or `TransactionMoreInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// TransactionMoreInfo(...).copyWithNull(firstField: true, secondField: true) - /// ```` - TransactionMoreInfo copyWithNull({ - bool note = false, - bool valueInDestiny = false, - bool transferAccount = false, - }) { - return TransactionMoreInfo( - note: note == true ? null : this.note, - tags: tags, - valueInDestiny: valueInDestiny == true ? null : this.valueInDestiny, - transferAccount: transferAccount == true ? null : this.transferAccount, - ); - } -} diff --git a/lib/app/transactions/form/dialogs/transaction_title_modal.dart b/lib/app/transactions/form/dialogs/transaction_title_modal.dart deleted file mode 100644 index 5fa2ad51..00000000 --- a/lib/app/transactions/form/dialogs/transaction_title_modal.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; -import 'package:monekin/core/presentation/widgets/modal_container.dart'; -import 'package:monekin/core/utils/constants.dart'; -import 'package:monekin/i18n/translations.g.dart'; - -Future showTransactionTitleModal( - BuildContext context, { - required String? initialTitle, -}) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) { - return TransactionTitleModal( - initialTitle: initialTitle, - ); - }); -} - -class TransactionTitleModal extends StatefulWidget { - const TransactionTitleModal({super.key, required this.initialTitle}); - - final String? initialTitle; - - @override - State createState() => _TransactionTitleModalState(); -} - -class _TransactionTitleModalState extends State { - FocusNode titleFocusNode = FocusNode(); - - TextEditingController titleController = TextEditingController(); - - @override - void initState() { - super.initState(); - - titleController.text = widget.initialTitle ?? ''; - titleFocusNode.requestFocus(); - } - - @override - void dispose() { - titleFocusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - - return ModalContainer( - title: t.transaction.form.title, - bodyPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - body: TextFormField( - controller: titleController, - maxLength: maxLabelLenghtForDisplayNames, - decoration: - InputDecoration(label: Text(t.transaction.form.title_short)), - focusNode: titleFocusNode, - ), - footer: BottomSheetFooter( - onSaved: () => Navigator.pop(context, titleController.text), - ), - ); - } -} diff --git a/lib/app/transactions/form/dialogs/transaction_value_in_destiny_modal.dart b/lib/app/transactions/form/dialogs/transaction_value_in_destiny_modal.dart deleted file mode 100644 index 5df65998..00000000 --- a/lib/app/transactions/form/dialogs/transaction_value_in_destiny_modal.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:monekin/core/database/app_db.dart'; -import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; -import 'package:monekin/core/presentation/widgets/modal_container.dart'; -import 'package:monekin/core/utils/text_field_utils.dart'; -import 'package:monekin/i18n/translations.g.dart'; - -Future showTransactionValueInDestinyModal( - BuildContext context, { - required num initialValue, - required CurrencyInDB currency, -}) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) { - return TransactionValueInDestinyModal( - initialValue: initialValue, - currency: currency, - ); - }, - ); -} - -class TransactionValueInDestinyModal extends StatefulWidget { - const TransactionValueInDestinyModal( - {super.key, required this.initialValue, required this.currency}); - - final num initialValue; - final CurrencyInDB currency; - - @override - State createState() => - _TransactionValueInDestinyModalState(); -} - -class _TransactionValueInDestinyModalState - extends State { - FocusNode valueInputFocusNode = FocusNode(); - - TextEditingController valueController = TextEditingController(); - - @override - void initState() { - super.initState(); - - valueController.text = widget.initialValue.toString(); - valueInputFocusNode.requestFocus(); - } - - @override - void dispose() { - valueInputFocusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - - return ModalContainer( - title: t.transfer.form.value_in_destiny.title, - bodyPadding: - const EdgeInsets.only(bottom: 12, left: 16, right: 16, top: 8), - body: TextFormField( - controller: valueController, - keyboardType: TextInputType.number, - inputFormatters: decimalDigitFormatter, - decoration: InputDecoration( - label: Text(t.transfer.form.value_in_destiny.title), - suffixText: widget.currency.symbol, - ), - focusNode: valueInputFocusNode, - onChanged: (value) { - setState(() {}); - }, - ), - footer: BottomSheetFooter( - onSaved: double.tryParse(valueController.text) == null - ? null - : () => Navigator.pop(context, double.parse(valueController.text)), - ), - ); - } -} diff --git a/lib/app/transactions/form/transaction_form.page.dart b/lib/app/transactions/form/transaction_form.page.dart index 96785e58..0a952218 100644 --- a/lib/app/transactions/form/transaction_form.page.dart +++ b/lib/app/transactions/form/transaction_form.page.dart @@ -3,17 +3,13 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:monekin/app/accounts/account_selector.dart'; import 'package:monekin/app/categories/selectors/category_picker.dart'; -import 'package:monekin/app/transactions/form/dialogs/transaction_more_info.modal.dart'; +import 'package:monekin/app/transactions/form/dialogs/amount_selector.dart'; import 'package:monekin/app/transactions/form/dialogs/transaction_status_selector.dart'; -import 'package:monekin/app/transactions/form/dialogs/transaction_title_modal.dart'; -import 'package:monekin/app/transactions/form/dialogs/transaction_value_in_destiny_modal.dart'; -import 'package:monekin/app/transactions/form/widgets/transaction_form_calculator.dart'; import 'package:monekin/core/database/app_db.dart'; import 'package:monekin/core/database/services/account/account_service.dart'; -import 'package:monekin/core/database/services/currency/currency_service.dart'; import 'package:monekin/core/database/services/exchange-rate/exchange_rate_service.dart'; import 'package:monekin/core/database/services/transaction/transaction_service.dart'; -import 'package:monekin/core/extensions/string.extension.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; import 'package:monekin/core/models/account/account.dart'; import 'package:monekin/core/models/category/category.dart'; import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; @@ -21,26 +17,41 @@ import 'package:monekin/core/models/tags/tag.dart'; import 'package:monekin/core/models/transaction/recurrency_data.dart'; import 'package:monekin/core/models/transaction/transaction.dart'; import 'package:monekin/core/models/transaction/transaction_status.enum.dart'; -import 'package:monekin/core/models/transaction/transaction_type.enum.dart'; -import 'package:monekin/core/presentation/animations/shake/shake_widget.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; -import 'package:monekin/core/presentation/widgets/modal_container.dart'; -import 'package:monekin/core/presentation/widgets/monekin_popup_menu_button.dart'; +import 'package:monekin/core/presentation/animations/animated_expanded.dart'; +import 'package:monekin/core/presentation/animations/scaled_animated_switcher.dart'; +import 'package:monekin/core/presentation/animations/shake_widget.dart'; +import 'package:monekin/core/presentation/responsive/breakpoint_container.dart'; +import 'package:monekin/core/presentation/responsive/breakpoints.dart'; +import 'package:monekin/core/presentation/widgets/inline_info_card.dart'; import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_displayer.dart'; +import 'package:monekin/core/presentation/widgets/persistent_footer_button.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; +import 'package:monekin/core/utils/constants.dart'; import 'package:monekin/core/utils/date_time_picker.dart'; +import 'package:monekin/core/utils/focus.dart'; +import 'package:monekin/core/utils/text_field_utils.dart'; import 'package:monekin/core/utils/uuid.dart'; import 'package:monekin/i18n/translations.g.dart'; -import 'widgets/account_or_category_selector.dart'; +import '../../../core/models/transaction/transaction_type.enum.dart'; +import '../../tags/tags_selector.modal.dart'; -enum TransactionFormMode { transfer, incomeOrExpense } +openTransactionFormDialog(BuildContext context, TransactionFormPage widget) { + return showDialog( + context: context, + builder: (context) { + return widget; + }); +} class TransactionFormPage extends StatefulWidget { - const TransactionFormPage( - {super.key, - this.mode = TransactionType.E, - this.fromAccount, - this.transactionToEdit}); + const TransactionFormPage({ + super.key, + this.mode = TransactionType.E, + this.fromAccount, + this.transactionToEdit, + }); + final TransactionType mode; final MoneyTransaction? transactionToEdit; @@ -51,29 +62,40 @@ class TransactionFormPage extends StatefulWidget { State createState() => _TransactionFormPageState(); } -class _TransactionFormPageState extends State { - late TransactionType transactionType; - final _shakeKey = GlobalKey(); +class _TransactionFormPageState extends State + with TickerProviderStateMixin { + final _formKey = GlobalKey(); - Account? fromAccount; + double transactionValue = 0; + + TextEditingController valueInDestinyController = TextEditingController(); + double? get valueInDestinyToNumber => + double.tryParse(valueInDestinyController.text); Category? selectedCategory; - DateTime date = DateTime.now(); + Account? fromAccount; + Account? transferAccount; - double transactionAmount = 0; + DateTime date = DateTime.now(); TransactionStatus? status; - String? title; + TextEditingController notesController = TextEditingController(); + TextEditingController titleController = TextEditingController(); bool get isEditMode => widget.transactionToEdit != null; RecurrencyData recurrentRule = const RecurrencyData.noRepeat(); - List get tags => moreInfo.tags; + List tags = []; + + late TransactionType transactionType; + + final _shakeKey = GlobalKey(); - TransactionMoreInfo moreInfo = const TransactionMoreInfo(); + late TabController _tabController; + final _mainContainerRadius = 12.0; @override void initState() { @@ -81,38 +103,110 @@ class _TransactionFormPageState extends State { transactionType = widget.mode; - if (transactionType == TransactionType.E) { - transactionAmount = transactionAmount * -1; - } + _tabController = TabController( + length: 3, + initialIndex: transactionType.index, + vsync: this, + ); + + _tabController.addListener(() { + transactionType = TransactionType.values.elementAt(_tabController.index); + + if (transactionType.isTransfer && transactionValue.isNegative) { + transactionValue = transactionValue * -1; + } + + setState(() {}); + }); if (widget.transactionToEdit != null) { fillForm(widget.transactionToEdit!); - } else { - AccountService.instance - .getAccounts( - predicate: (acc, curr) => AppDB.instance.buildExpr([ - acc.type.equalsValue(AccountType.saving).not(), - acc.closingDate.isNull() - ]), - limit: transactionType.isTransfer ? 2 : 1) - .first - .then((acc) { - fromAccount = widget.fromAccount ?? acc[0]; - - if (transactionType.isTransfer) { - moreInfo = moreInfo.copyWith( - transferAccount: acc[1].id != fromAccount!.id ? acc[1] : acc[0], - ); - } - setState(() {}); - }); + return; } + + AccountService.instance + .getAccounts( + predicate: (acc, curr) => AppDB.instance.buildExpr([ + acc.type.equalsValue(AccountType.saving).not(), + acc.closingDate.isNull() + ]), + limit: transactionType.isTransfer ? 2 : 1, + ) + .first + .then((acc) { + fromAccount = widget.fromAccount ?? acc[0]; + + WidgetsBinding.instance.addPostFrameCallback((_) { + displayAmountModal(context); + }); + + if (widget.mode.isTransfer) { + transferAccount = (acc[1].id != fromAccount!.id ? acc[1] : acc[0]); + } + + setState(() {}); + }); + } + + Widget selector( + {required String title, + required String? inputValue, + required Widget icon, + required Function onClick, + required BorderRadius? borderRadius}) { + final t = Translations.of(context); + + return InkWell( + onTap: () { + unfocusCurrentFocusedItem(context); + onClick(); + }, + borderRadius: borderRadius, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + icon, + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + softWrap: false, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium!.copyWith( + fontWeight: FontWeight.w300, + ), + ), + Text( + inputValue ?? t.general.unspecified, + softWrap: false, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w600, + ), + ) + ], + ), + ) + ], + ), + ), + ); } submitForm() { + print("HOLAALLALA"); + print(transactionType.isTransfer); + print(transferAccount); if (transactionType.isIncomeOrExpense && selectedCategory == null || - transactionType.isTransfer && moreInfo.transferAccount == null) { + transactionType.isTransfer && transferAccount == null) { _shakeKey.currentState?.shake(); return; } @@ -120,7 +214,7 @@ class _TransactionFormPageState extends State { final t = Translations.of(context); final scMessenger = ScaffoldMessenger.of(context); - if (transactionAmount == 0) { + if (transactionValue == 0) { scMessenger.showSnackBar( SnackBar(content: Text(t.transaction.form.validators.zero)), ); @@ -128,7 +222,7 @@ class _TransactionFormPageState extends State { return; } - if (transactionAmount < 0 && transactionType.isTransfer) { + if (transactionValue < 0 && transactionType.isTransfer) { scMessenger.showSnackBar(SnackBar( content: Text(t.transaction.form.validators.negative_transfer), )); @@ -136,33 +230,16 @@ class _TransactionFormPageState extends State { return; } - // Transactions after account creation: if (fromAccount != null && fromAccount!.date.compareTo(date) > 0) { scMessenger.showSnackBar( SnackBar( - content: - Text(t.transaction.form.validators.date_after_account_creation), - ), - ); - - return; - } - - // In transfers, we can not have the same source and destination account: - if (transactionType.isTransfer && - fromAccount!.id == moreInfo.transferAccount!.id) { - scMessenger.showSnackBar( - SnackBar( - content: Text( - t.transaction.form.validators.transfer_between_same_accounts), - ), + content: Text( + t.transaction.form.validators.date_after_account_creation)), ); return; } - // VALIDATIONS ENDED -- PROCEED: - onSuccess() { Navigator.pop(context); @@ -179,28 +256,29 @@ class _TransactionFormPageState extends State { TransactionInDB( id: newTrID, date: date, + type: transactionType, accountID: fromAccount!.id, - value: transactionAmount, + value: transactionType.isIncomeOrExpense && + selectedCategory!.type.isExpense + ? transactionValue * -1 + : transactionValue, isHidden: false, - type: transactionType, status: date.compareTo(DateTime.now()) > 0 ? TransactionStatus.pending : status, - notes: moreInfo.note.notEmptyString, - title: title.notEmptyString, + notes: notesController.text.isEmpty ? null : notesController.text, + title: titleController.text.isEmpty ? null : titleController.text, intervalEach: recurrentRule.intervalEach, intervalPeriod: recurrentRule.intervalPeriod, endDate: recurrentRule.ruleRecurrentLimit?.endDate, remainingTransactions: recurrentRule.ruleRecurrentLimit?.remainingIterations, - valueInDestiny: transactionType.isTransfer && - moreInfo.valueInDestiny != transactionAmount - ? moreInfo.valueInDestiny?.toDouble() - : null, + valueInDestiny: + transactionType.isTransfer ? valueInDestinyToNumber : null, categoryID: transactionType.isIncomeOrExpense ? selectedCategory?.id : null, receivingAccountID: - transactionType.isTransfer ? moreInfo.transferAccount?.id : null, + transactionType.isTransfer ? transferAccount?.id : null, ), ) .then((value) { @@ -230,142 +308,6 @@ class _TransactionFormPageState extends State { }); } - fillForm(MoneyTransaction transaction) async { - setState( - () { - fromAccount = transaction.account; - date = transaction.date; - status = transaction.status; - selectedCategory = transaction.category; - recurrentRule = transaction.recurrentInfo; - - if (selectedCategory != null && - selectedCategory!.type == CategoryType.B) { - if (transaction.value < 0) { - selectedCategory!.type = CategoryType.E; - } else { - selectedCategory!.type = CategoryType.I; - } - } - - moreInfo = TransactionMoreInfo( - tags: transaction.tags, - note: transaction.notes, - valueInDestiny: transaction.isTransfer - ? transaction.valueInDestiny ?? transaction.value - : null, - transferAccount: transaction.receivingAccount, - ); - }, - ); - - title = transaction.title; - transactionAmount = transaction.value; - } - - Card buildAccoutAndCategorySelectorRow(BuildContext context) { - return Card( - shape: RoundedRectangleBorder( - // side: BorderSide(color: Theme.of(context).dividerColor, width: 2), - borderRadius: BorderRadius.circular(12), - ), - margin: const EdgeInsets.all(0), - elevation: 0, - color: AppColors.of(context).surface, - clipBehavior: Clip.hardEdge, - child: LayoutBuilder(builder: (context, constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: constraints.maxWidth * 0.5), - child: AccountOrCategorySelector( - title: t.general.account, - inputValue: fromAccount?.name, - icon: fromAccount?.displayIcon(context) ?? - IconDisplayer( - displayMode: IconDisplayMode.polygon, - icon: Icons.question_mark_rounded, - mainColor: AppColors.of(context).primary, - ), - onClick: () async { - final modalRes = await showAccountSelector(fromAccount!); - - if (modalRes != null && modalRes.isNotEmpty) { - setState(() { - fromAccount = modalRes.first; - }); - } - }), - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 0, horizontal: 4), - child: Icon(Icons.arrow_forward, size: 16), - ), - if (transactionType == TransactionType.T) - Flexible( - child: ShakeWidget( - duration: const Duration(milliseconds: 200), - shakeCount: 1, - shakeOffset: 10, - key: _shakeKey, - child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: constraints.maxWidth * 0.5), - child: AccountOrCategorySelector( - title: t.transfer.form.to, - inputValue: moreInfo.transferAccount?.name, - icon: moreInfo.transferAccount?.displayIcon(context) ?? - IconDisplayer( - displayMode: IconDisplayMode.polygon, - icon: Icons.question_mark_rounded, - mainColor: AppColors.of(context).primary, - ), - onClick: () async { - final modalRes = await showAccountSelector( - moreInfo.transferAccount); - - if (modalRes != null && modalRes.isNotEmpty) { - moreInfo = moreInfo.copyWith( - transferAccount: modalRes.first); - setState(() {}); - } - }), - ), - ), - ), - if (transactionType != TransactionType.T) - Flexible( - child: ShakeWidget( - duration: const Duration(milliseconds: 200), - shakeCount: 1, - shakeOffset: 10, - key: _shakeKey, - child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: constraints.maxWidth * 0.5), - child: AccountOrCategorySelector( - title: t.general.category, - inputValue: selectedCategory?.name, - icon: IconDisplayer.fromCategory( - context, - category: selectedCategory ?? - Category.fromDB(Category.unkown(), null), - size: 24, - ), - onClick: () => selectCategory()), - ), - ), - ), - ], - ); - }), - ); - } - Future?> showAccountSelector(Account? account) { return showAccountSelectorBottomSheet( context, @@ -382,11 +324,9 @@ class _TransactionFormPageState extends State { context, modal: CategoryPicker( selectedCategory: selectedCategory, - categoryType: [ - CategoryType.B, - if (transactionType == TransactionType.E) CategoryType.E, - if (transactionType == TransactionType.I) CategoryType.I - ], + categoryType: transactionType == TransactionType.E + ? [CategoryType.E] + : [CategoryType.I], ), ); @@ -397,430 +337,616 @@ class _TransactionFormPageState extends State { } } + fillForm(MoneyTransaction transaction) async { + setState(() { + fromAccount = transaction.account; + transferAccount = transaction.receivingAccount; + date = transaction.date; + status = transaction.status; + selectedCategory = transaction.category; + recurrentRule = transaction.recurrentInfo; + + if (selectedCategory != null && + selectedCategory!.type == CategoryType.B) { + if (transaction.value < 0) { + selectedCategory!.type = CategoryType.E; + } else { + selectedCategory!.type = CategoryType.I; + } + } + + tags = [...transaction.tags]; + }); + + notesController.text = transaction.notes ?? ''; + titleController.text = transaction.title ?? ''; + transactionValue = transaction.value; + transactionType = transaction.type; + + if (transactionType == TransactionType.E) { + transactionValue = transactionValue * -1; + } + + valueInDestinyController.text = + transaction.valueInDestiny?.abs().toString() ?? ''; + } + + Widget buildValueInDestinyFormField() { + return ListTile( + leading: const Icon(Icons.trending_flat_rounded), + minTileHeight: 64, + title: TextFormField( + controller: valueInDestinyController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: t.transfer.form.value_in_destiny.title, + suffixText: transferAccount?.currency.symbol, + filled: false, + isDense: false, + ), + keyboardType: TextInputType.number, + inputFormatters: decimalDigitFormatter, + validator: (value) { + final defaultNumberValidatorResult = fieldValidator(value, + isRequired: false, validator: ValidatorType.double); + + if (defaultNumberValidatorResult != null) { + return defaultNumberValidatorResult; + } + + return null; + }, + autovalidateMode: AutovalidateMode.onUserInteraction, + textInputAction: TextInputAction.next, + onChanged: (value) { + setState(() {}); + }, + ), + ); + } + + Widget buildStatusSelector() { + final isSelectorDisabled = date.compareTo(DateTime.now()) > 0; + + final selectedStatus = + isSelectorDisabled ? TransactionStatus.pending : status; + + return ListTile( + leading: ScaledAnimatedSwitcher( + keyToWatch: selectedStatus.icon.toString(), + child: Icon( + selectedStatus.icon, + color: + (selectedStatus?.color ?? Theme.of(context).colorScheme.primary) + .withOpacity(isSelectorDisabled ? 0.3 : 1), + ), + ), + minTileHeight: 64, + title: Text(selectedStatus.displayName(context)), + enabled: !isSelectorDisabled, + onTap: () { + unfocusCurrentFocusedItem(context); + + showTransactioStatusModal(context, initialStatus: status) + .then((modalRes) { + if (modalRes == null) return; + + setState(() { + status = modalRes.result; + }); + }); + }, + ); + } + + Widget buildRecurrencySelectorField() { + return ListTile( + leading: Icon( + recurrentRule.isRecurrent ? Icons.repeat_rounded : Icons.repeat_one, + ), + minTileHeight: 64, + title: Text(recurrentRule.formText(context)), + onTap: () { + showIntervalSelectoHelpDialog(context, + selectedRecurrentRule: recurrentRule, + onRecurrentRuleSelected: (res) { + setState(() { + recurrentRule = res; + }); + }); + }, + ); + } + + Widget buildDescriptionField() { + return ListTile( + leading: const Icon(Icons.description_rounded), + minTileHeight: 64, + titleAlignment: ListTileTitleAlignment.titleHeight, + title: TextFormField( + controller: notesController, + minLines: 2, + maxLines: 10, + decoration: InputDecoration( + isDense: false, + filled: false, + border: InputBorder.none, + hintText: t.transaction.form.description_info, + ), + ), + ); + } + + Widget buildTitleField() { + return ListTile( + leading: const Icon(Icons.title_rounded), + title: TextFormField( + controller: titleController, + maxLength: maxLabelLenghtForDisplayNames, + decoration: InputDecoration( + isDense: false, + filled: false, + counterText: '', + border: InputBorder.none, + hintText: t.transaction.form.title, + ), + ), + ); + } + @override Widget build(BuildContext context) { + final t = Translations.of(context); + + final formFieldWithDividers = [ + buildTitleField(), + const Divider(), + buildTransactionDateSelector(), + const Divider(), + buildRecurrencySelectorField(), + const Divider(), + buildStatusSelector(), + const Divider(), + buildTransactionTagsSelector(), + const Divider(), + if (transactionType.isTransfer) ...[ + buildValueInDestinyFormField(), + const Divider(), + ], + buildDescriptionField(), + const Divider(), + ]; + return Scaffold( - resizeToAvoidBottomInset: false, appBar: AppBar( - centerTitle: true, - title: SegmentedButton( - style: SegmentedButton.styleFrom( - selectedBackgroundColor: transactionType.color(context), - selectedForegroundColor: Colors.white, + title: Text( + isEditMode + ? t.transaction.edit + : transactionType == TransactionType.T + ? t.transfer.create + : transactionType == TransactionType.E + ? t.transaction.new_expense + : t.transaction.new_income, + ), + backgroundColor: transactionType.color(context).withOpacity(0.85), + foregroundColor: Colors.white, + elevation: 0, + bottom: TabBar( + indicatorColor: Colors.white, + labelColor: Colors.white, + labelStyle: const TextStyle(fontWeight: FontWeight.w700), + unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.normal), + unselectedLabelColor: Colors.white.withOpacity(0.8), + tabAlignment: TabAlignment.fill, + dividerColor: transactionType.color(context).darken(0.3), + controller: _tabController, + tabs: TransactionType.values + .map((tType) => Tab( + text: tType.displayName(context), + )) + .toList(), + isScrollable: false, + ), + ), + persistentFooterButtons: [ + PersistentFooterButton( + child: FilledButton.icon( + onPressed: () { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + + submitForm(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.general.validations.form_error)), + ); + } + }, + icon: const Icon(Icons.save), + label: Text(isEditMode ? t.transaction.edit : t.transaction.create), ), - showSelectedIcon: false, - segments: TransactionType.values - .map( - (e) => ButtonSegment( - value: e, - icon: Icon(e.mathIcon), - tooltip: e.displayName(context), + ) + ], + body: Form( + key: _formKey, + child: BreakpointContainer( + lgChild: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + buildAmountContainer(context), + buildAccoutAndCategorySelectorRow(context), + ], + ), + ), + ], + ), + ), + const VerticalDivider(thickness: 2), + Expanded( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 16), + child: Column( + children: formFieldWithDividers, + ), + ), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + buildAmountContainer(context), + buildAccoutAndCategorySelectorRow(context), + + // const Divider(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.only(top: 4, bottom: 12), + child: Column( + children: formFieldWithDividers, + ), ), ) - .toList(), - selected: {transactionType}, - onSelectionChanged: (newMode) { - transactionType = newMode.first; - - if (transactionType == TransactionType.E && - !transactionAmount.isNegative) { - transactionAmount = transactionAmount * -1; - } else if (transactionType != TransactionType.E && - transactionAmount.isNegative) { - // Transfers and incomes -> Convert to positive - transactionAmount = transactionAmount * -1; - } - - setState(() {}); - }, + ], + ), ), - actions: [ - MonekinPopupMenuButton(actionItems: [ - //TODO - ]) - ], ), - body: Column( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), - child: Column( + ); + } + + void displayAmountModal(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => AmountSelector( + title: t.transaction.form.value, + initialAmount: transactionValue, + enableSignToggleButton: transactionType.isIncomeOrExpense, + currency: fromAccount?.currency, + onSubmit: (amount) { + setState(() { + transactionValue = amount; + Navigator.pop(context); + }); + }, + ), + ); + } + + Widget buildAmountContainer(BuildContext context) { + return Tappable( + bgColor: transactionType.color(context).withOpacity(0.85), + onTap: () => displayAmountModal(context), + borderRadius: BreakPoint.of(context).isLargerThan(BreakpointID.md) + ? BorderRadius.only( + topLeft: Radius.circular(_mainContainerRadius), + topRight: Radius.circular(_mainContainerRadius), + ) + : null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + child: DefaultTextStyle( + style: const TextStyle(color: Colors.white), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: StreamBuilder( - stream: - CurrencyService.instance.getUserPreferredCurrency(), - builder: (context, snapshot) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buildDatePickerButtons(context), - const SizedBox(height: 8), - AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: Theme.of(context) - .textTheme - .headlineLarge! - .copyWith( - fontWeight: FontWeight.w800, - color: transactionType.color(context), - fontSize: transactionAmount.abs() >= 1000 - ? transactionAmount.abs() >= 1000000 - ? 32 - : 42 - : 56, - ), - child: CurrencyDisplayer( - amountToConvert: transactionAmount, - followPrivateMode: false, - currency: fromAccount?.currency, - ), - ), - if (fromAccount != null && - snapshot.hasData && - snapshot.data!.code != - fromAccount!.currency.code) - // Display exchange rate to preferred currency if needed - buildTrAmountInPrefCurrency() - ], - ); - }, + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: IconDisplayer( + key: ValueKey(transactionType.mathIcon.toString()), + mainColor: transactionType.color(context), + secondaryColor: Colors.white, + padding: 2, + borderRadius: 4, + icon: transactionType.mathIcon, ), ), - Padding( - padding: const EdgeInsets.only(left: 4, bottom: 8), + AnimatedDefaultTextStyle( + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontSize: transactionValue >= 1000 + ? transactionValue >= 1000000 + ? 28 + : 34 + : 38), + duration: const Duration(milliseconds: 200), child: Builder(builder: (context) { - final buttonStyle = TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - width: 2, - color: AppColors.of(context).brand, - ), - ), - iconColor: AppColors.of(context).brand, - foregroundColor: AppColors.of(context).brand, - backgroundColor: - AppColors.of(context).brand.withOpacity(0.2), + const bigTextStyle = TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white, ); - final _valueInDestinyOrDefault = - moreInfo.valueInDestiny ?? transactionAmount; - - return Row( - // mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (transactionType.isTransfer && - moreInfo.transferAccount != null && - (moreInfo.valueInDestiny != transactionAmount && - transactionAmount > 0 && - moreInfo.valueInDestiny != null || - moreInfo.transferAccount!.currencyId != - fromAccount?.currencyId)) - TextButton.icon( - style: buttonStyle, - onPressed: () { - showTransactionValueInDestinyModal( - context, - initialValue: _valueInDestinyOrDefault, - currency: - moreInfo.transferAccount!.currency, - ).then((value) { - if (value != null) { - setState(() { - moreInfo = moreInfo.copyWith( - valueInDestiny: value, - ); - }); - } - }); - }, - icon: const Icon(Icons.start_rounded), - label: Text('${NumberFormat.currency( - symbol: - moreInfo.transferAccount!.currency.symbol, - decimalDigits: - _valueInDestinyOrDefault % 1 == 0 ? 0 : 2, - ).format(_valueInDestinyOrDefault)} a cuenta de destino')), - if (transactionType == TransactionType.E && - !transactionAmount.isNegative || - transactionType == TransactionType.I && - transactionAmount.isNegative) - TextButton.icon( - style: buttonStyle, - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) { - return ModalContainer( - title: t.transaction.reversed.title, - titleBuilder: (title) { - return Row( - children: [ - Icon( - MoneyTransaction.reversedIcon, - size: 28, - ), - const SizedBox(width: 12), - Text(title) - ], - ); - }, - bodyPadding: const EdgeInsets.only( - left: 16, - bottom: 8, - right: 8, - ), - body: Text( - transactionType == TransactionType.I - ? t.transaction.reversed - .description_for_incomes - : t.transaction.reversed - .description_for_expenses), - ); - }, - ); - }, - label: Text(t.transaction.reversed.title_short), - icon: Icon(MoneyTransaction.reversedIcon), - ), - ], + return CurrencyDisplayer( + amountToConvert: transactionValue, + currency: fromAccount?.currency, + currencyStyle: bigTextStyle, + integerStyle: bigTextStyle, + followPrivateMode: false, ); }), ), - Column( - children: [ - buildAccoutAndCategorySelectorRow(context), - const Divider(height: 4), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: buildTitleButton(context)), - const SizedBox(width: 12), - buildExtraInfoButtons(context) - ], - ), - ], - ) ], ), - ), + if (fromAccount != null) + StreamBuilder( + stream: ExchangeRateService.instance + .calculateExchangeRateToPreferredCurrency( + fromCurrency: fromAccount!.currency.code, + amount: transactionValue, + ), + builder: (context, exchangeRateSnapshot) { + final shouldHide = !exchangeRateSnapshot.hasData || + exchangeRateSnapshot.data! == transactionValue; + + final valueInPrefCurrencyIndicator = Column( + children: [ + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Icon( + Icons.swap_horizontal_circle_rounded, + size: 14, + ), + const SizedBox(width: 4), + CurrencyDisplayer( + amountToConvert: exchangeRateSnapshot.data ?? 0, + integerStyle: const TextStyle( + fontWeight: FontWeight.w300, + color: Colors.white, + ), + followPrivateMode: false, + ), + ], + ), + ], + ); + + return AnimatedSizeSwitcher( + duration: const Duration(milliseconds: 400), + child: !shouldHide + ? valueInPrefCurrencyIndicator + : const SizedBox.shrink(), + ); + }) + ], ), - Container( - height: 275, - padding: const EdgeInsets.all(0), - decoration: BoxDecoration( - color: AppColors.of(context).light, - ), - child: TransactionFormCalculator( - amountToConvert: transactionAmount, - showNegativeToggleButton: transactionType.isIncomeOrExpense, - onSubmit: () => submitForm(), - onChange: (amount) { - setState(() { - transactionAmount = amount; - }); - }, - ), - ) - ], + ), ), ); } - /// Build an indicator with the transaction value in the currency of the user - StreamBuilder buildTrAmountInPrefCurrency() { - return StreamBuilder( - stream: - ExchangeRateService.instance.calculateExchangeRateToPreferredCurrency( - fromCurrency: fromAccount!.currency.code, - amount: transactionAmount, - ), - builder: (context, exchangeRateSnapshot) { - if (!exchangeRateSnapshot.hasData || - exchangeRateSnapshot.data! == transactionAmount) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - const SizedBox(height: 8), - CurrencyDisplayer( - amountToConvert: exchangeRateSnapshot.data!, - followPrivateMode: false, - integerStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: AppColors.of(context).onSurface.withAlpha(200)), - ), - ], + Widget buildTransactionTagsSelector() { + final Widget tagsChips = Wrap( + spacing: 6, + runSpacing: 0, + children: List.generate(tags.length, (index) { + final tag = tags[index]; + + return FilterChip( + label: Text( + tag.name, + style: TextStyle(color: tag.colorData), + ), + selected: true, + onSelected: (value) => setState(() { + tags.removeWhere((element) => element.id == tag.id); + }), + showCheckmark: false, + selectedColor: tag.colorData.lighten(0.8), + avatar: tag.displayIcon(), ); - }, + }), ); - } - Row buildExtraInfoButtons(BuildContext context) { - final t = Translations.of(context); + return ListTile( + leading: Icon(Tag.icon), + minTileHeight: 64, + onTap: () { + showTagListModal(context, + modal: TagSelector( + selectedTags: tags, + allowEmptySubmit: true, + includeNullTag: false, + )).then( + (value) { + if (value == null) { + return; + } - return Row( - children: [ - Builder(builder: (context) { - final selectedStatus = date.compareTo(DateTime.now()) > 0 - ? TransactionStatus.pending - : status; - - return IconButton.outlined( - icon: Icon( - selectedStatus.icon, - color: (selectedStatus?.color ?? AppColors.of(context).primary) - .withOpacity(date.compareTo(DateTime.now()) > 0 ? 0.3 : 1), - ), - tooltip: t.transaction.status.display_long, - onPressed: date.compareTo(DateTime.now()) > 0 - ? null - : () => showTransactioStatusModal( - context, - initialStatus: selectedStatus, - ).then((modalRes) { - if (modalRes == null) return; - - setState(() { - status = modalRes.result; - }); - }), + setState(() { + tags = value.nonNulls.toList(); + }); + }, ); - }), - const SizedBox(width: 6), - IconButton.outlined( - onPressed: () => showTransactionMoreInfoModal( - context, - data: moreInfo.copyWith( - valueInDestiny: transactionType.isTransfer - ? moreInfo.valueInDestiny ?? transactionAmount - : null, - ), - ).then((modalRes) { - if (modalRes == null) return; - - setState(() { - moreInfo = modalRes; - }); - }), - icon: const Icon(Icons.notes_rounded), - ), - ], - ); + }, + title: tags.isEmpty + ? Text( + t.tags.select.title, + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.75), + ), + ) + : tagsChips); } - Widget buildDatePickerButtons(BuildContext context) { - final buttonStyle = TextButton.styleFrom( - backgroundColor: AppColors.of(context).light, - ); + Widget buildTransactionDateSelector() { + final dateFormat = date.year == currentYear + ? DateFormat.MMMMd().add_jm() + : DateFormat.yMMMd().add_jm(); - return Row( - mainAxisAlignment: MainAxisAlignment.center, + return Column( children: [ - TextButton( - style: buttonStyle, - onPressed: () { - openDateTimePicker( + ListTile( + leading: const Icon(Icons.event), + minTileHeight: 64, + title: Text(dateFormat.format(date)), + onTap: () async { + unfocusCurrentFocusedItem(context); + + final datePickerRes = await openDateTimePicker( context, - showTimePickerAfterDate: false, initialDate: date, - ).then((res) { - if (res == null) return; + showTimePickerAfterDate: true, + ); + if (datePickerRes != null) { setState(() { - date = date.copyWith( - day: res.day, - year: res.year, - month: res.month, - isUtc: res.isUtc, - ); + date = datePickerRes; }); - }); + } }, - child: Text( - DateFormat.yMMMd().format(date), - ), ), - const SizedBox(width: 4), - TextButton( - style: buttonStyle, - onPressed: () { - showTimePicker( - context: context, - initialTime: TimeOfDay.fromDateTime(date), - ).then((res) { - if (res == null) return; - - setState(() { - date = date.copyWith( - hour: res.hour, - minute: res.minute, - ); - }); - }); - }, - child: Text( - DateFormat.Hm().format(date), + if (date.compareTo(DateTime.now()) > 0) + InlineInfoCard( + margin: const EdgeInsets.fromLTRB(12, 8, 12, 16), + text: t.transaction.form.validators.date_max, + mode: InlineInfoCardMode.info, + ), + if (fromAccount != null && + fromAccount!.date.compareTo(date) > 0 && + !(date.compareTo(DateTime.now()) > 0)) + InlineInfoCard( + margin: const EdgeInsets.fromLTRB(12, 8, 12, 16), + text: t.transaction.form.validators.date_after_account_creation, + mode: InlineInfoCardMode.warn, ), - ), - const SizedBox(width: 4), - TextButton( - style: buttonStyle, - onPressed: () { - showIntervalSelectoHelpDialog(context, - selectedRecurrentRule: recurrentRule, - onRecurrentRuleSelected: (res) { - setState(() { - recurrentRule = res; - }); - }); - }, - child: - Icon(recurrentRule.isRecurrent ? Icons.repeat : Icons.repeat_one), - ) ], ); } - TextButton buildTitleButton(BuildContext context) { - return TextButton( - onPressed: () => showTransactionTitleModal( - context, - initialTitle: title, - ).then((modalRes) { - if (modalRes == null) return; + Widget buildAccoutAndCategorySelectorRow(BuildContext context) { + final borderRadius = Radius.circular(_mainContainerRadius); - setState(() { - title = modalRes; - }); - }), - style: TextButton.styleFrom( - backgroundColor: Colors.transparent, - surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 8, + return DecoratedBox( + decoration: BoxDecoration( + color: transactionType.color(context).withOpacity(0.35), + borderRadius: BorderRadius.only( + bottomLeft: borderRadius, + bottomRight: borderRadius, + ), ), - alignment: Alignment.centerLeft, - ), - child: Text( - title.notEmptyString ?? t.transaction.form.title, - softWrap: false, - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontWeight: - title.isNotNullNorEmpty ? FontWeight.w400 : FontWeight.w300, - ), - ), - ); + child: SizedBox( + height: 74, + child: Row( + children: [ + ...[ + Expanded( + flex: 1, + child: selector( + title: t.general.account, + inputValue: fromAccount?.name, + borderRadius: BorderRadius.only(bottomLeft: borderRadius), + icon: fromAccount?.displayIcon(context) ?? + IconDisplayer( + displayMode: IconDisplayMode.polygon, + icon: Icons.question_mark_rounded, + mainColor: Theme.of(context).colorScheme.primary, + ), + onClick: () async { + final modalRes = + await showAccountSelector(fromAccount!); + + if (modalRes != null && modalRes.isNotEmpty) { + setState(() { + fromAccount = modalRes.first; + }); + } + }), + ), + VerticalDivider( + color: transactionType.color(context).withOpacity(0.85), + thickness: 2, + ) + ], + if (transactionType.isTransfer) + Expanded( + flex: 1, + child: ShakeWidget( + duration: const Duration(milliseconds: 200), + shakeCount: 1, + shakeOffset: 10, + key: _shakeKey, + child: selector( + title: t.transfer.form.to, + inputValue: transferAccount?.name, + borderRadius: + BorderRadius.only(bottomRight: borderRadius), + icon: transferAccount?.displayIcon(context) ?? + IconDisplayer( + displayMode: IconDisplayMode.polygon, + icon: Icons.question_mark_rounded, + mainColor: Theme.of(context).colorScheme.primary, + ), + onClick: () async { + final modalRes = + await showAccountSelector(transferAccount); + + if (modalRes != null && modalRes.isNotEmpty) { + setState(() { + transferAccount = modalRes.first; + }); + } + }), + ), + ), + if (!transactionType.isTransfer) + Expanded( + flex: 1, + child: ShakeWidget( + duration: const Duration(milliseconds: 200), + shakeCount: 1, + shakeOffset: 10, + key: _shakeKey, + child: selector( + title: t.general.category, + inputValue: selectedCategory?.name, + borderRadius: + BorderRadius.only(bottomRight: borderRadius), + icon: IconDisplayer.fromCategory( + context, + category: selectedCategory ?? + Category.fromDB(Category.unkown(), null), + size: 24, + ), + onClick: () => selectCategory(), + ), + ), + ), + ], + ), + )); } } diff --git a/lib/app/transactions/form/widgets/transaction_form_calculator.dart b/lib/app/transactions/form/widgets/transaction_form_calculator.dart deleted file mode 100644 index e95dc379..00000000 --- a/lib/app/transactions/form/widgets/transaction_form_calculator.dart +++ /dev/null @@ -1,308 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:monekin/core/extensions/color.extensions.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; - -/// String that identifies the remove character button -const _removeButtonID = '⌫'; - -class TransactionFormCalculator extends StatefulWidget { - const TransactionFormCalculator( - {super.key, - required this.amountToConvert, - this.onChange, - this.onSubmit, - this.showNegativeToggleButton = true}); - - final double amountToConvert; - - final bool showNegativeToggleButton; - - /// If the calculator will start with a negative value -//final bool initiallyNegative; - - final void Function(double amount)? onChange; - final void Function()? onSubmit; - - @override - State createState() => - _TransactionFormCalculatorState(); -} - -class _TransactionFormCalculatorState extends State { - late String selectedAmount; - double get valueToNumber => double.tryParse(selectedAmount) ?? 0; - - late bool isNegative; - - final FocusNode _focusNode = FocusNode(); - late FocusAttachment _focusAttachment; - - @override - void initState() { - super.initState(); - - if (widget.amountToConvert == 0) { - selectedAmount = '0'; - } else { - selectedAmount = - removeTrailingZeroes(widget.amountToConvert.toStringAsFixed(2)); - } - - isNegative = widget.amountToConvert.isNegative; - - _focusAttachment = _focusNode.attach(context, onKeyEvent: (node, event) { - bool keyIsPressed = event.runtimeType == KeyDownEvent || - event.runtimeType == KeyRepeatEvent; - - if (!keyIsPressed) { - return KeyEventResult.handled; - } - - if ((event.logicalKey == LogicalKeyboardKey.browserBack || - event.logicalKey == LogicalKeyboardKey.goBack || - event.logicalKey == LogicalKeyboardKey.escape)) { - Navigator.pop(context); - } - - for (final (index, element) in [ - LogicalKeyboardKey.digit0, - LogicalKeyboardKey.digit1, - LogicalKeyboardKey.digit2, - LogicalKeyboardKey.digit3, - LogicalKeyboardKey.digit4, - LogicalKeyboardKey.digit5, - LogicalKeyboardKey.digit6, - LogicalKeyboardKey.digit7, - LogicalKeyboardKey.digit8, - LogicalKeyboardKey.digit9, - ].indexed) { - if (event.logicalKey == element) { - addToAmount(index.toStringAsFixed(0)); - break; - } - } - - for (final (index, element) in [ - LogicalKeyboardKey.numpad0, - LogicalKeyboardKey.numpad1, - LogicalKeyboardKey.numpad2, - LogicalKeyboardKey.numpad3, - LogicalKeyboardKey.numpad4, - LogicalKeyboardKey.numpad5, - LogicalKeyboardKey.numpad6, - LogicalKeyboardKey.numpad7, - LogicalKeyboardKey.numpad8, - LogicalKeyboardKey.numpad9, - ].indexed) { - if (event.logicalKey == element) { - addToAmount(index.toStringAsFixed(0)); - break; - } - } - - if (event.logicalKey == LogicalKeyboardKey.period || - event.logicalKey == LogicalKeyboardKey.numpadDecimal || - event.logicalKey == LogicalKeyboardKey.comma) { - addToAmount('.'); - } else if (event.logicalKey == LogicalKeyboardKey.backspace || - event.logicalKey == LogicalKeyboardKey.delete) { - onButtonPress(_removeButtonID); - } else if (event.logicalKey == LogicalKeyboardKey.enter && - widget.onSubmit != null) { - widget.onSubmit!(); - } - - return KeyEventResult.handled; - }); - - _focusNode.requestFocus(); - } - - @override - void didUpdateWidget(oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.amountToConvert != widget.amountToConvert || - oldWidget.amountToConvert.isNegative != - widget.amountToConvert.isNegative) { - setState(() { - isNegative = widget.amountToConvert.isNegative; - }); - } - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - String removeTrailingZeroes(String input) { - if (!input.contains('.')) { - return input; - } - int index = input.length - 1; - while (input[index] == '0') { - index--; - } - if (input[index] == '.') { - index--; - } - return input.substring(0, index + 1); - } - - void addToAmount(String text) { - final decimalPlaces = selectedAmount.split('.').elementAtOrNull(1); - - if (decimalPlaces != null && decimalPlaces.length >= 2) { - return; - } - - updateSelectedAmount(selectedAmount + text); - } - - void updateSelectedAmount(String newAmount) { - setState(() { - selectedAmount = - "${isNegative ? '-' : ''}${newAmount.replaceAll('-', '')}"; - - if (widget.onChange != null) { - widget.onChange!(valueToNumber); - } - }); - } - - void onButtonPress(String text) { - HapticFeedback.lightImpact(); - - if (text == 'DONE') { - if (widget.onSubmit != null) { - widget.onSubmit!(); - } - - return; - } - - if (text == 'AC') { - updateSelectedAmount('0'); - } else if (text == _removeButtonID) { - if (valueToNumber == 0) return; - - updateSelectedAmount( - selectedAmount.substring(0, selectedAmount.length - 1)); - } else if (text == '-') { - isNegative = !isNegative; - updateSelectedAmount(selectedAmount); - } else { - addToAmount(text); - } - } - - Widget buildCalculatorButton( - BuildContext context, { - required String text, - int flex = 1, - Color? bgColor, - Color? textColor, - }) { - textColor ??= AppColors.of(context).onSurface; - bgColor ??= Colors.transparent; - - return Expanded( - flex: flex, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 0), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: bgColor, - shadowColor: bgColor.darken(0.15), - surfaceTintColor: bgColor.darken(0.15), - foregroundColor: textColor, - disabledForegroundColor: textColor.withOpacity(0.3), - disabledBackgroundColor: bgColor.withOpacity(0.3), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(0), - ), - elevation: 0, - ), - onPressed: text == 'DONE' && (valueToNumber == 0) - ? null - : () => onButtonPress(text), - child: text == _removeButtonID || text == 'DONE' - ? Icon(text == _removeButtonID - ? Icons.backspace_rounded - : Icons.check_rounded) - : text == '-' - ? const Icon(Icons.exposure_rounded) - : Text( - text, - softWrap: false, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - _focusAttachment.reparent(); - - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildCalculatorButton(context, text: '1'), - buildCalculatorButton(context, text: '4'), - buildCalculatorButton(context, text: '7'), - buildCalculatorButton(context, text: '.'), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildCalculatorButton(context, text: '2'), - buildCalculatorButton(context, text: '5'), - buildCalculatorButton(context, text: '8'), - buildCalculatorButton(context, text: '0'), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildCalculatorButton(context, text: '3'), - buildCalculatorButton(context, text: '6'), - buildCalculatorButton(context, text: '9'), - buildCalculatorButton(context, text: _removeButtonID), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildCalculatorButton(context, text: 'AC'), - if (widget.showNegativeToggleButton) - buildCalculatorButton(context, text: '-'), - buildCalculatorButton(context, - bgColor: AppColors.of(context).primary, - text: 'DONE', - textColor: AppColors.of(context).onPrimary, - flex: widget.showNegativeToggleButton ? 2 : 3), - ], - ), - ), - ], - ); - } -} diff --git a/lib/app/transactions/label_value_info_list.dart b/lib/app/transactions/label_value_info_list.dart index 78688053..070f5365 100644 --- a/lib/app/transactions/label_value_info_list.dart +++ b/lib/app/transactions/label_value_info_list.dart @@ -1,7 +1,5 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:monekin/app/transactions/label_value_info_table.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; class LabelValueInfoListItem extends LabelValueInfoItem { final Widget? trailing; @@ -20,19 +18,28 @@ class LabelValueInfoList extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: items.mapIndexed((index, element) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: items.length, + separatorBuilder: (context, index) { + return const Divider( + // color: Colors.grey, // Customize the color of the separator + thickness: 1, // Customize the thickness of the separator + indent: 16, + endIndent: 16, + ); + }, + itemBuilder: (context, index) { + final element = items[index]; return ListTile( minVerticalPadding: 0, contentPadding: const EdgeInsets.symmetric(horizontal: 16), title: Text(element.label), subtitle: element.value, trailing: element.trailing, - tileColor: index % 2 != 0 - ? AppColors.of(context).surface - : Theme.of(context).colorScheme.surfaceContainerLowest, ); - }).toList(), + }, ); } } diff --git a/lib/app/transactions/label_value_info_table.dart b/lib/app/transactions/label_value_info_table.dart index 7c69be41..4ef2427e 100644 --- a/lib/app/transactions/label_value_info_table.dart +++ b/lib/app/transactions/label_value_info_table.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; class LabelValueInfoItem { final Widget value; @@ -19,45 +18,46 @@ class LabelValueInfoTable extends StatelessWidget { @override Widget build(BuildContext context) { - return Table( - border: TableBorder(borderRadius: BorderRadius.circular(0)), - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(3), - 1: FlexColumnWidth(7), - }, - children: items - .mapIndexed( - (i, e) => TableRow( - decoration: BoxDecoration( - color: i % 2 != 0 - ? AppColors.of(context).surface - : Theme.of(context).colorScheme.surfaceContainerLowest, - ), - children: [ - TableCell( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - child: Text( - e.label, - style: const TextStyle(fontWeight: FontWeight.w300), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Table( + border: TableBorder( + borderRadius: BorderRadius.circular(0), + horizontalInside: + BorderSide(width: 1, color: Theme.of(context).dividerColor)), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(3), + 1: FlexColumnWidth(7), + }, + children: items + .mapIndexed( + (i, e) => TableRow( + children: [ + TableCell( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 4, + ), + child: Text( + e.label, + style: const TextStyle(fontWeight: FontWeight.w300), + ), ), ), - ), - TableCell( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: e.value, + TableCell( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 2, vertical: 4), + child: e.value, + ), ), - ), - ], - ), - ) - .toList(), + ], + ), + ) + .toList(), + ), ); } } diff --git a/lib/app/transactions/transaction_details.page.dart b/lib/app/transactions/transaction_details.page.dart index b4eca038..33e30c1d 100644 --- a/lib/app/transactions/transaction_details.page.dart +++ b/lib/app/transactions/transaction_details.page.dart @@ -259,7 +259,7 @@ class _TransactionDetailsPageState extends State { subtitleTextStyle: Theme.of(context).textTheme.labelSmall!.copyWith( color: isNext ? transaction.nextPayStatus!.color(context).darken(0.6) - : AppColors.of(context).primaryContainer, + : Theme.of(context).colorScheme.primaryContainer, ), leading: Icon( isNext ? transaction.nextPayStatus!.icon : Icons.access_time, @@ -274,7 +274,7 @@ class _TransactionDetailsPageState extends State { transaction.nextPayStatus! .displayDaysToPay(context, transaction.daysToPay()), style: TextStyle( - color: AppColors.of(context).onSurface, + color: Theme.of(context).colorScheme.onSurface, ), ), trailing: Row(mainAxisSize: MainAxisSize.min, children: [ @@ -419,8 +419,8 @@ class _TransactionDetailsPageState extends State { final color = showRecurrencyStatus ? isDarkTheme - ? AppColors.of(context).primary - : AppColors.of(context).primary.lighten(0.2) + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.primary.lighten(0.2) : transaction.status!.color; return translucentCard( @@ -581,7 +581,9 @@ class _TransactionDetailsPageState extends State { value: buildInfoTileWithIconAndColor( icon: transaction .receivingAccount!.icon, - color: AppColors.of(context).primary, + color: Theme.of(context) + .colorScheme + .primary, data: transaction .receivingAccount!.name, ), @@ -790,7 +792,7 @@ class _TransactionDetailsPageState extends State { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: AppColors.of(context).onSurface.withOpacity(0.85)), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.85)), ), ); } @@ -833,7 +835,7 @@ class _TransactionDetailHeader extends SliverPersistentHeaderDelegate { final shrinkPercent = shrinkOffset / maxExtent; return Container( - color: AppColors.of(context).surface, + color: Theme.of(context).colorScheme.surface, padding: const EdgeInsets.only(left: 24, right: 24, top: 16, bottom: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/app/transactions/transactions.page.dart b/lib/app/transactions/transactions.page.dart index 01f10a75..087bff26 100644 --- a/lib/app/transactions/transactions.page.dart +++ b/lib/app/transactions/transactions.page.dart @@ -3,13 +3,12 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:monekin/app/home/widgets/new_transaction_fl_button.dart'; import 'package:monekin/app/layout/tabs.dart'; -import 'package:monekin/app/transactions/form/transaction_form.page.dart'; import 'package:monekin/app/transactions/widgets/bulk_edit_transaction_modal.dart'; import 'package:monekin/app/transactions/widgets/transaction_list.dart'; import 'package:monekin/core/database/services/transaction/transaction_service.dart'; import 'package:monekin/core/models/transaction/transaction.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; import 'package:monekin/core/presentation/widgets/confirm_dialog.dart'; import 'package:monekin/core/presentation/widgets/filter_row_indicator.dart'; import 'package:monekin/core/presentation/widgets/monekin_popup_menu_button.dart'; @@ -18,7 +17,6 @@ import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_ import 'package:monekin/core/presentation/widgets/skeleton.dart'; import 'package:monekin/core/presentation/widgets/transaction_filter/filter_sheet_modal.dart'; import 'package:monekin/core/presentation/widgets/transaction_filter/transaction_filters.dart'; -import 'package:monekin/core/routes/route_utils.dart'; import 'package:monekin/core/utils/list_tile_action_item.dart'; import 'package:monekin/i18n/translations.g.dart'; @@ -145,14 +143,7 @@ class _TransactionsPageState extends State { icon: const Icon(Icons.filter_alt_outlined)), ], ), - floatingActionButton: FloatingActionButton.extended( - icon: const Icon(Icons.add_rounded), - label: Text(t.transaction.create), - onPressed: () => RouteUtils.pushRoute( - context, - const TransactionFormPage(), - ), - ), + floatingActionButton: const NewTransactionButton(isExtended: true), body: Column( children: [ if (filters.hasFilter) ...[ @@ -177,7 +168,7 @@ class _TransactionsPageState extends State { return Card( elevation: 2, - //color: AppColors.of(context).primary, + //color: Theme.of(context).colorScheme.primary, margin: const EdgeInsets.all(8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), @@ -277,8 +268,8 @@ class _TransactionsPageState extends State { AppBar selectedTransactionsAppbar() { return AppBar( - backgroundColor: AppColors.of(context).primary, - foregroundColor: AppColors.of(context).onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, leading: IconButton( onPressed: () { cleanSelectedTransactions(); diff --git a/lib/app/transactions/widgets/transaction_list_tile.dart b/lib/app/transactions/widgets/transaction_list_tile.dart index 9a4304ec..3a0c6c61 100644 --- a/lib/app/transactions/widgets/transaction_list_tile.dart +++ b/lib/app/transactions/widgets/transaction_list_tile.dart @@ -94,7 +94,7 @@ class TransactionListTile extends StatelessWidget { Icon( transaction.status?.icon ?? Icons.repeat, color: transaction.status?.color.darken(0.1) ?? - AppColors.of(context).primary, + Theme.of(context).colorScheme.primary, size: 12, ) ], @@ -240,14 +240,15 @@ class TransactionListTile extends StatelessWidget { Icon( Icons.check, size: 24, - color: AppColors.of(context).surface, + color: Theme.of(context).colorScheme.surface, ), ], ) : transaction.getDisplayIcon(context, size: 28, padding: 6), ), selected: isSelected, - selectedTileColor: AppColors.of(context).primary.withOpacity(0.15), + selectedTileColor: + Theme.of(context).colorScheme.primary.withOpacity(0.15), onTap: onTap ?? () { RouteUtils.pushRoute( diff --git a/lib/core/database/services/account/account_service.dart b/lib/core/database/services/account/account_service.dart index f4768f4e..64bd6db5 100644 --- a/lib/core/database/services/account/account_service.dart +++ b/lib/core/database/services/account/account_service.dart @@ -127,16 +127,25 @@ class AccountService { final initialBalanceQuery = db .customSelect( """ - SELECT COALESCE(SUM(accounts.iniValue ${convertToPreferredCurrency ? ' * COALESCE(excRate.exchangeRate, 1)' : ''} ), 0) AS balance - FROM accounts - ${convertToPreferredCurrency ? _joinAccountAndRate(date) : ''} - ${accountIds != null ? 'WHERE accounts.id IN (${List.filled(accountIds.length, '?').join(', ')})' : ''} - """, + SELECT COALESCE( + SUM( + CASE WHEN accounts.date > ? THEN 0 + ELSE accounts.iniValue + ${convertToPreferredCurrency ? ' * COALESCE(excRate.exchangeRate, 1)' : ''} + END + ) + , 0) + AS balance + FROM accounts + ${convertToPreferredCurrency ? _joinAccountAndRate(date) : ''} + ${accountIds != null ? 'WHERE accounts.id IN (${List.filled(accountIds.length, '?').join(', ')})' : ''} + """, readsFrom: { db.accounts, if (convertToPreferredCurrency) db.exchangeRates }, variables: [ + Variable.withDateTime(date), if (convertToPreferredCurrency) Variable.withDateTime(date), if (accountIds != null) for (final id in accountIds) Variable.withString(id) diff --git a/lib/core/extensions/bool.extension.dart b/lib/core/extensions/bool.extension.dart new file mode 100644 index 00000000..61f41828 --- /dev/null +++ b/lib/core/extensions/bool.extension.dart @@ -0,0 +1,21 @@ +extension BoolConversions on bool { + /// Convert to integer (1 for true, 0 for false) + int toInt() { + return this ? 1 : 0; + } + + /// Convert to double (1.0 for true, 0.0 for false) + double toDouble() { + return this ? 1.0 : 0.0; + } + + /// Chainable 'and' operation + bool and(bool other) { + return this && other; + } + + /// Chainable 'or' operation + bool or(bool other) { + return this || other; + } +} diff --git a/lib/core/extensions/color.extensions.dart b/lib/core/extensions/color.extensions.dart index 0aaf3454..3c3194e0 100644 --- a/lib/core/extensions/color.extensions.dart +++ b/lib/core/extensions/color.extensions.dart @@ -32,7 +32,7 @@ extension ColorBrightness on Color { assert(amount >= -1 && amount <= 1); if (amount < 0) { - return lighten(amount.abs()); + return darken(amount.abs()); } return Color.fromARGB( diff --git a/lib/core/extensions/lists.extensions.dart b/lib/core/extensions/lists.extensions.dart index 08307d47..7739b673 100644 --- a/lib/core/extensions/lists.extensions.dart +++ b/lib/core/extensions/lists.extensions.dart @@ -44,3 +44,15 @@ extension PrintListItem on Iterable { return join(', '); } } + +extension ListEqualityCheck on List { + bool allItemsEqual() { + if (isEmpty) { + return true; // Return true for an empty list as a convention + } + + // Compare all elements to the first one + final firstItem = this[0]; + return every((item) => item == firstItem); + } +} diff --git a/lib/core/extensions/numbers.extensions.dart b/lib/core/extensions/numbers.extensions.dart index af97b45e..f3a71836 100644 --- a/lib/core/extensions/numbers.extensions.dart +++ b/lib/core/extensions/numbers.extensions.dart @@ -12,4 +12,13 @@ extension FileFormatter on num { return '${NumberFormat.decimalPatternDigits(decimalDigits: 1).format(this / pow(base, digitGroups))} ${units[digitGroups]}'; } + + double roundWithDecimals(int decimalPlaces) { + if (isInfinite || isNaN) { + return toDouble(); + } + + num mod = pow(10.0, decimalPlaces); + return ((this * mod).round().toDouble() / mod); + } } diff --git a/lib/core/models/account/account.dart b/lib/core/models/account/account.dart index 325df96e..0f78bce6 100644 --- a/lib/core/models/account/account.dart +++ b/lib/core/models/account/account.dart @@ -79,8 +79,8 @@ class Account extends AccountInDB { return color != null ? ColorHex.get(color!) : Theme.of(context).brightness == Brightness.dark - ? AppColors.of(context).primaryContainer - : AppColors.of(context).primary; + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.primary; } IconDisplayer displayIcon( diff --git a/lib/core/models/transaction/next_pay_status.enum.dart b/lib/core/models/transaction/next_pay_status.enum.dart index 93993bbc..9c15cea7 100644 --- a/lib/core/models/transaction/next_pay_status.enum.dart +++ b/lib/core/models/transaction/next_pay_status.enum.dart @@ -14,7 +14,7 @@ enum NextPayStatus { Color color(BuildContext context) { if (this == planified) { - return AppColors.of(context).primary; + return Theme.of(context).colorScheme.primary; } else if (this == delayed) { return AppColors.of(context).danger; } diff --git a/lib/core/presentation/animations/animated_expanded.dart b/lib/core/presentation/animations/animated_expanded.dart index c98ac626..9a8ecfdb 100644 --- a/lib/core/presentation/animations/animated_expanded.dart +++ b/lib/core/presentation/animations/animated_expanded.dart @@ -1,8 +1,23 @@ import 'package:flutter/material.dart'; +/// A widget that smoothly expands or collapses its child with an animation. +/// +/// The animation can be configured to expand either vertically or horizontally +/// and includes both size and fade transitions. +/// +/// [AnimatedExpanded] is useful for cases where you want to dynamically show +/// or hide content with a smooth animation, such as expanding a section of a +/// list or a collapsible panel. +/// +/// The widget automatically listens for changes to the [expand] property and +/// triggers the animation accordingly. class AnimatedExpanded extends StatefulWidget { + /// The widget to display inside the animated container. final Widget child; + + /// A boolean flag indicating whether to expand or collapse the [child] final bool expand; + final Duration duration; final Curve sizeCurve; final Axis axis; @@ -85,39 +100,42 @@ class _AnimatedExpandedState extends State } } -// Animated Switcher may be needed, if old data gets wiped immediately -// we want to keep the old UI when a transition occurs to make it smoother +/// A widget that switches between two children with an animated size transition. +/// +/// [AnimatedSizeSwitcher] ensures that the old widget remains visible until the new one +/// has fully transitioned in, making the transition smoother. It uses [AnimatedSwitcher] +/// internally, with a custom transition that animates the size of the child widget. class AnimatedSizeSwitcher extends StatelessWidget { const AnimatedSizeSwitcher({ required this.child, - this.sizeCurve = Curves.easeInOutCubicEmphasized, - this.sizeDuration = const Duration(milliseconds: 800), - this.switcherDuration = const Duration(milliseconds: 250), - this.sizeAlignment = AlignmentDirectional.center, - this.clipBehavior = Clip.hardEdge, + this.duration = const Duration(milliseconds: 250), this.enabled = true, + this.axis = Axis.vertical, super.key, }); + final Widget child; - final Curve sizeCurve; - final Duration sizeDuration; - final Duration switcherDuration; - final AlignmentDirectional sizeAlignment; - final Clip clipBehavior; + final Duration duration; final bool enabled; + final Axis axis; @override Widget build(BuildContext context) { if (enabled == false) return child; - return AnimatedSize( - clipBehavior: clipBehavior, - duration: sizeDuration, - curve: sizeCurve, - alignment: sizeAlignment, - child: AnimatedSwitcher( - duration: switcherDuration, - child: child, - ), + + return AnimatedSwitcher( + switchInCurve: Curves.fastEaseInToSlowEaseOut, + switchOutCurve: Curves.fastOutSlowIn, + duration: duration, + transitionBuilder: (child, animation) { + return SizeTransition( + axisAlignment: 1, + sizeFactor: animation, + axis: axis, + child: child, + ); + }, + child: child, ); } } diff --git a/lib/core/presentation/animations/shake/fade_in.dart b/lib/core/presentation/animations/fade_in.dart similarity index 100% rename from lib/core/presentation/animations/shake/fade_in.dart rename to lib/core/presentation/animations/fade_in.dart diff --git a/lib/core/presentation/animations/scaled_animated_switcher.dart b/lib/core/presentation/animations/scaled_animated_switcher.dart new file mode 100644 index 00000000..8af310a3 --- /dev/null +++ b/lib/core/presentation/animations/scaled_animated_switcher.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class ScaledAnimatedSwitcher extends StatelessWidget { + const ScaledAnimatedSwitcher({ + required this.keyToWatch, + required this.child, + this.duration = const Duration(milliseconds: 450), + super.key, + }); + + final String keyToWatch; + final Widget child; + final Duration duration; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: duration, + switchInCurve: Curves.easeInOutCubic, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + final fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: animation, + curve: const Interval(0.5, 1), + ), + ); + + final scaleAnimation = Tween(begin: 0, end: 1.0).animate( + CurvedAnimation( + parent: animation, + curve: const Interval(0, 1.0), + ), + ); + + return FadeTransition( + opacity: fadeAnimation, + child: ScaleTransition( + alignment: Alignment.center, + scale: scaleAnimation, + child: child, + ), + ); + }, + child: SizedBox(key: ValueKey(keyToWatch), child: child), + ); + } +} diff --git a/lib/core/presentation/animations/shake/shake_widget.dart b/lib/core/presentation/animations/shake_widget.dart similarity index 100% rename from lib/core/presentation/animations/shake/shake_widget.dart rename to lib/core/presentation/animations/shake_widget.dart diff --git a/lib/core/presentation/app_colors.dart b/lib/core/presentation/app_colors.dart index 581b8838..74bb96a5 100644 --- a/lib/core/presentation/app_colors.dart +++ b/lib/core/presentation/app_colors.dart @@ -7,39 +7,21 @@ class AppColors extends ThemeExtension { const AppColors({ required this.danger, required this.success, + required this.brand, required this.light, required this.dark, required this.shadowColor, required this.shadowColorLight, - required this.brand, - required this.inputFill, - required this.primary, - required this.onPrimary, - required this.primaryContainer, - required this.onPrimaryContainer, - required this.surface, - required this.onSurface, - required this.modalBackground, }); final Color danger; final Color success; final Color brand; - final Color inputFill; final Color light; final Color dark; final Color shadowColor; final Color shadowColorLight; - /* ---- From the material color scheme: ---- */ - final Color primary; - final Color onPrimary; - final Color primaryContainer; - final Color onPrimaryContainer; - final Color surface; - final Color onSurface; - final Color modalBackground; - static AppColors fromColorScheme(ColorScheme colorScheme) { final isDark = colorScheme.brightness == Brightness.dark; @@ -48,30 +30,14 @@ class AppColors extends ThemeExtension { success: isDark ? Colors.lightGreen : const Color.fromARGB(255, 55, 161, 59), brand: isDark ? const Color.fromARGB(255, 128, 134, 177) : brandBlue, - light: colorScheme.surfaceContainerLow, - dark: colorScheme.inverseSurface, - shadowColor: isDark ? const Color.fromARGB(105, 189, 189, 189) : const Color.fromARGB(100, 90, 90, 90), - shadowColorLight: isDark ? const Color.fromARGB(40, 116, 116, 116) : const Color.fromARGB(44, 90, 90, 90), - - inputFill: colorScheme.surfaceContainerHighest, - - // Colors from the material color scheme: - primary: colorScheme.primary, - onPrimary: colorScheme.onPrimary, - primaryContainer: colorScheme.primaryContainer, - onPrimaryContainer: colorScheme.onPrimaryContainer, - surface: colorScheme.surface, - onSurface: colorScheme.onSurface, - - modalBackground: colorScheme.surfaceContainer, ); } @@ -84,35 +50,21 @@ class AppColors extends ThemeExtension { Color? danger, Color? success, Color? brand, - Color? primary, Color? inputFill, Color? dark, Color? light, Color? shadowColor, Color? shadowColorLight, - Color? onPrimary, - Color? primaryContainer, - Color? onPrimaryContainer, - Color? surface, - Color? onSurface, Color? modalBackground, }) { return AppColors( danger: danger ?? this.danger, success: success ?? this.success, - inputFill: inputFill ?? this.inputFill, light: light ?? this.light, dark: dark ?? this.dark, brand: brand ?? this.brand, shadowColor: shadowColor ?? this.shadowColor, shadowColorLight: shadowColorLight ?? this.shadowColorLight, - primary: primary ?? this.primary, - onPrimary: onPrimary ?? this.onPrimary, - primaryContainer: primaryContainer ?? this.primaryContainer, - onPrimaryContainer: onPrimaryContainer ?? this.onPrimaryContainer, - surface: surface ?? this.surface, - onSurface: onSurface ?? this.onSurface, - modalBackground: modalBackground ?? this.modalBackground, ); } @@ -124,7 +76,6 @@ class AppColors extends ThemeExtension { return AppColors( danger: Color.lerp(danger, other.danger, t) ?? danger, success: Color.lerp(success, other.success, t) ?? success, - inputFill: Color.lerp(inputFill, other.inputFill, t) ?? inputFill, light: Color.lerp(light, other.light, t) ?? light, dark: Color.lerp(dark, other.dark, t) ?? dark, shadowColor: Color.lerp(shadowColor, other.shadowColor, t) ?? shadowColor, @@ -132,18 +83,29 @@ class AppColors extends ThemeExtension { Color.lerp(shadowColorLight, other.shadowColorLight, t) ?? shadowColorLight, brand: Color.lerp(brand, other.brand, t) ?? brand, - primary: Color.lerp(primary, other.primary, t) ?? primary, - onPrimary: Color.lerp(onPrimary, other.onPrimary, t) ?? onPrimary, - primaryContainer: - Color.lerp(primaryContainer, other.primaryContainer, t) ?? - primaryContainer, - onPrimaryContainer: - Color.lerp(onPrimaryContainer, other.onPrimaryContainer, t) ?? - onPrimaryContainer, - surface: Color.lerp(surface, other.surface, t) ?? surface, - onSurface: Color.lerp(onSurface, other.onSurface, t) ?? onSurface, - modalBackground: Color.lerp(modalBackground, other.modalBackground, t) ?? - modalBackground, ); } } + +extension CustomThemeDataExt on ThemeData { + CustomColorSchemeExtended get colorSchemeExtended { + return CustomColorSchemeExtended(colorScheme, brightness); + } +} + +class CustomColorSchemeExtended { + final ColorScheme _colorScheme; + final Brightness _brightness; + + CustomColorSchemeExtended(this._colorScheme, this._brightness); + + Color get modalBackground => _colorScheme.surfaceContainer; + Color get inputFill => _colorScheme.surfaceContainerHighest; + Color get cardColor => _colorScheme.surfaceContainer; + Color get dashboardHeader => _brightness == Brightness.light + ? _colorScheme.primary + : _colorScheme.primaryContainer; + Color get onDashboardHeader => _brightness == Brightness.light + ? _colorScheme.onPrimary + : _colorScheme.onPrimaryContainer; +} diff --git a/lib/core/presentation/theme.dart b/lib/core/presentation/theme.dart index d2c4a200..d9227b31 100644 --- a/lib/core/presentation/theme.dart +++ b/lib/core/presentation/theme.dart @@ -87,10 +87,11 @@ ThemeData getThemeData( return theme.copyWith( scaffoldBackgroundColor: theme.colorScheme.surface, dividerTheme: const DividerThemeData(space: 0), - cardColor: theme.colorScheme.surface, + cardColor: theme.colorSchemeExtended.cardColor, + cardTheme: CardTheme(color: theme.colorSchemeExtended.cardColor), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: theme.colorScheme.surfaceContainerHighest, + fillColor: theme.colorSchemeExtended.inputFill, isDense: true, floatingLabelStyle: TextStyle( backgroundColor: theme.colorScheme.surface.withOpacity(0.5), @@ -107,7 +108,7 @@ ThemeData getThemeData( bottomSheetTheme: theme.bottomSheetTheme.copyWith( elevation: 0, dragHandleSize: const Size(25, 4), - modalBackgroundColor: customAppColors.modalBackground, + modalBackgroundColor: theme.colorSchemeExtended.modalBackground, dragHandleColor: Colors.grey[300], clipBehavior: Clip.hardEdge, ), diff --git a/lib/core/presentation/widgets/animated_progress_bar.dart b/lib/core/presentation/widgets/animated_progress_bar.dart index 7763f53f..6046f02b 100644 --- a/lib/core/presentation/widgets/animated_progress_bar.dart +++ b/lib/core/presentation/widgets/animated_progress_bar.dart @@ -45,7 +45,7 @@ class _AnimatedProgressBarState extends State { topRight: Radius.circular(widget.radius), ); - final barColor = widget.color ?? AppColors.of(context).primary; + final barColor = widget.color ?? Theme.of(context).colorScheme.primary; return TweenAnimationBuilder( duration: Duration(milliseconds: widget.animationDuration), diff --git a/lib/core/presentation/widgets/card_with_header.dart b/lib/core/presentation/widgets/card_with_header.dart index 4fd15039..efa82724 100644 --- a/lib/core/presentation/widgets/card_with_header.dart +++ b/lib/core/presentation/widgets/card_with_header.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; - -import '../app_colors.dart'; +import 'package:monekin/i18n/translations.g.dart'; /// The radius of the `CardWithHeader` widget, a very useful widget through the app const cardWithHeaderRadius = 12.0; @@ -9,82 +8,59 @@ class CardWithHeader extends StatelessWidget { const CardWithHeader({ super.key, required this.title, + this.subtitle, required this.body, - this.onHeaderButtonClick, - this.headerButtonIcon = Icons.arrow_forward_ios_rounded, this.bodyPadding = const EdgeInsets.all(0), + this.footer, }); final Widget body; + final Widget? footer; final String title; - - final IconData headerButtonIcon; + final String? subtitle; final EdgeInsets bodyPadding; - final void Function()? onHeaderButtonClick; - @override Widget build(BuildContext context) { - const double iconSize = 16; - - return Container( + return Card( clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - color: AppColors.of(context).surface, - borderRadius: BorderRadius.circular(cardWithHeaderRadius), - boxShadow: [ - BoxShadow( - color: AppColors.of(context).shadowColorLight, - blurRadius: cardWithHeaderRadius, - offset: const Offset(0, 0), - spreadRadius: 4, - ), - ], - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(cardWithHeaderRadius), - border: Border.all( - width: 1, - color: Theme.of(context).dividerColor, - ), - ), margin: const EdgeInsets.all(0), + elevation: 0, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( clipBehavior: Clip.hardEdge, - padding: EdgeInsets.fromLTRB( - 16, - onHeaderButtonClick != null ? 2 : iconSize - 6, - 2, - onHeaderButtonClick != null ? 2 : iconSize - 6), - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( + padding: const EdgeInsets.fromLTRB(16, 12, 2, 4), + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), - color: AppColors.of(context).light, + // color: AppColors.of(context).light, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(title, - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w700)), - if (onHeaderButtonClick != null) - IconButton( - onPressed: onHeaderButtonClick, - iconSize: iconSize, - color: AppColors.of(context).primary, - icon: Icon(headerButtonIcon), - ) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.w600)), + if (subtitle != null) + Text( + subtitle!, + style: const TextStyle( + fontSize: 12, fontWeight: FontWeight.w300), + ), + ], + ), ], ), ), - const Divider(), Material( type: MaterialType.transparency, clipBehavior: Clip.antiAliasWithSaveLayer, @@ -92,9 +68,43 @@ class CardWithHeader extends StatelessWidget { padding: bodyPadding, child: body, ), - ) + ), + if (footer != null) footer! ], ), ); } } + +class CardFooterWithSingleButton extends StatelessWidget { + const CardFooterWithSingleButton({super.key, this.text, this.onButtonClick}); + + final String? text; + final VoidCallback? onButtonClick; + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider( + thickness: 2, + indent: 16, + endIndent: 16, + ), + Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.fromLTRB(2, 4, 2, 4), + child: TextButton.icon( + onPressed: onButtonClick, + iconAlignment: IconAlignment.end, + icon: const Icon(Icons.arrow_forward_ios_rounded, size: 14), + label: Text(text ?? t.general.see_more), + ), + ) + ], + ); + } +} diff --git a/lib/core/presentation/widgets/count_indicator.dart b/lib/core/presentation/widgets/count_indicator.dart index 8f38bb20..bfc0df09 100644 --- a/lib/core/presentation/widgets/count_indicator.dart +++ b/lib/core/presentation/widgets/count_indicator.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; class CountIndicatorWithExpandArrow extends StatelessWidget { const CountIndicatorWithExpandArrow({ @@ -46,13 +45,13 @@ class CountIndicator extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), - color: AppColors.of(context).primary, + color: Theme.of(context).colorScheme.primary, ), child: Text( countToDisplay.toString(), style: Theme.of(context).textTheme.labelSmall!.copyWith( fontWeight: fontWeight, - color: AppColors.of(context).onPrimary, + color: Theme.of(context).colorScheme.onPrimary, ), ), ); diff --git a/lib/core/presentation/widgets/currency_selector_modal.dart b/lib/core/presentation/widgets/currency_selector_modal.dart index 81b3dea7..6a95a8cd 100644 --- a/lib/core/presentation/widgets/currency_selector_modal.dart +++ b/lib/core/presentation/widgets/currency_selector_modal.dart @@ -69,7 +69,7 @@ class _CurrencySelectorModalState extends State { title: t.currencies.select_a_currency, endWidget: Chip( side: BorderSide(color: colors.primary, width: 2), - // backgroundColor: AppColors.of(context).primaryLight, + // backgroundColor: Theme.of(context).colorScheme.primaryLight, label: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -171,7 +171,7 @@ class _CurrencySelectorModalState extends State { }, ), ScrollableWithBottomGradient.buildPositionedGradient( - AppColors.of(context).modalBackground), + Theme.of(context).colorSchemeExtended.modalBackground), ]), ), ], diff --git a/lib/core/presentation/widgets/dates/outlinedButtonStacked.dart b/lib/core/presentation/widgets/dates/outlinedButtonStacked.dart index b006bfc8..559a47c3 100644 --- a/lib/core/presentation/widgets/dates/outlinedButtonStacked.dart +++ b/lib/core/presentation/widgets/dates/outlinedButtonStacked.dart @@ -42,7 +42,7 @@ class OutlinedButtonStacked extends StatelessWidget { Widget build(BuildContext context) { return Tappable( onTap: onTap, - borderRadius: borderRadius, + borderRadius: BorderRadius.circular(borderRadius), bgColor: Colors.transparent, child: _OutlinedContainer( filled: filled, diff --git a/lib/core/presentation/widgets/dates/segmented_calendar_button.dart b/lib/core/presentation/widgets/dates/segmented_calendar_button.dart index ad7d8850..f1b5333d 100644 --- a/lib/core/presentation/widgets/dates/segmented_calendar_button.dart +++ b/lib/core/presentation/widgets/dates/segmented_calendar_button.dart @@ -58,10 +58,11 @@ class _SegmentedCalendarButtonState extends State { onPressed: disabled ? null : onPressed, icon: Icon(icon), iconSize: widget.buttonHeight - padding * 2, - disabledColor: AppColors.of(context).primary, - color: AppColors.of(context).primary, + disabledColor: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.primary, style: IconButton.styleFrom( - side: BorderSide(color: AppColors.of(context).primary, width: 2), + side: BorderSide( + color: Theme.of(context).colorScheme.primary, width: 2), fixedSize: Size.fromHeight(widget.buttonHeight), padding: EdgeInsets.all(padding), minimumSize: const Size.fromHeight(0), diff --git a/lib/core/presentation/widgets/expansion_panel/single_expansion_panel.dart b/lib/core/presentation/widgets/expansion_panel/single_expansion_panel.dart index ad5b3500..7ec3ad89 100644 --- a/lib/core/presentation/widgets/expansion_panel/single_expansion_panel.dart +++ b/lib/core/presentation/widgets/expansion_panel/single_expansion_panel.dart @@ -3,8 +3,11 @@ import 'package:monekin/core/presentation/widgets/expansion_panel/expansion_pane import 'package:monekin/i18n/translations.g.dart'; class SingleExpansionPanel extends StatefulWidget { - const SingleExpansionPanel( - {super.key, required this.child, this.sidePadding = 0}); + const SingleExpansionPanel({ + super.key, + required this.child, + this.sidePadding = 0, + }); final Widget child; final double sidePadding; @@ -29,11 +32,14 @@ class _SingleExpansionPanelState extends State { }, children: [ ExpansionPanel( + backgroundColor: Theme.of(context).colorScheme.surface, // canTapOnHeader: true, headerBuilder: (context, isExpanded) { return Padding( padding: EdgeInsets.symmetric( - vertical: 0, horizontal: widget.sidePadding), + vertical: 0, + horizontal: widget.sidePadding, + ), child: Row( children: [ const Expanded(child: Divider()), diff --git a/lib/core/presentation/widgets/icon_selector_modal.dart b/lib/core/presentation/widgets/icon_selector_modal.dart index 84d483d5..499e361e 100644 --- a/lib/core/presentation/widgets/icon_selector_modal.dart +++ b/lib/core/presentation/widgets/icon_selector_modal.dart @@ -47,8 +47,6 @@ class _IconSelectorModalState extends State { @override Widget build(BuildContext context) { - final AppColors colors = AppColors.of(context); - final t = Translations.of(context); return DraggableScrollableSheet( @@ -60,7 +58,8 @@ class _IconSelectorModalState extends State { final iconsByScope = SupportedIconService.instance.getIconsByScope(); return Scaffold( - backgroundColor: AppColors.of(context).modalBackground, + backgroundColor: + Theme.of(context).colorSchemeExtended.modalBackground, body: Column(children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), @@ -79,17 +78,21 @@ class _IconSelectorModalState extends State { ], ), Chip( - side: BorderSide(color: colors.primary, width: 2), - // backgroundColor: AppColors.of(context).primaryLight, - label: _selectedIcon! - .display(size: 34, color: colors.onSurface), + side: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2), + // backgroundColor: Theme.of(context).colorScheme.primaryLight, + label: _selectedIcon!.display( + size: 34, + color: Theme.of(context).colorScheme.onSurface), ) ], ), ), Expanded( child: ScrollableWithBottomGradient( - gradientColor: AppColors.of(context).modalBackground, + gradientColor: + Theme.of(context).colorSchemeExtended.modalBackground, controller: scrollController, child: Column( children: iconsByScope.keys.toList().map((scope) { @@ -103,7 +106,9 @@ class _IconSelectorModalState extends State { Container( padding: const EdgeInsets.symmetric( vertical: 0, horizontal: 16), - color: colors.modalBackground, + color: Theme.of(context) + .colorSchemeExtended + .modalBackground, child: Text(t[ 'icon_selector.scopes.${scope.replaceAll("/", "_")}']), ), @@ -122,7 +127,9 @@ class _IconSelectorModalState extends State { : 1, clipBehavior: Clip.antiAlias, color: _selectedIcon?.id == e.id - ? colors.primary + ? Theme.of(context) + .colorScheme + .primary : null, child: IconDisplayer( supportedIcon: e, @@ -135,8 +142,12 @@ class _IconSelectorModalState extends State { secondaryColor: Colors.transparent, isOutline: _selectedIcon?.id == e.id, mainColor: _selectedIcon?.id == e.id - ? colors.onPrimary - : colors.onSurface), + ? Theme.of(context) + .colorScheme + .onPrimary + : Theme.of(context) + .colorScheme + .onSurface), )) .toList(), ), diff --git a/lib/core/presentation/widgets/inline_info_card.dart b/lib/core/presentation/widgets/inline_info_card.dart index 1eba63e9..6c8cba47 100644 --- a/lib/core/presentation/widgets/inline_info_card.dart +++ b/lib/core/presentation/widgets/inline_info_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; import 'package:monekin/core/presentation/responsive/responsive_row_column.dart'; - -import '../app_colors.dart'; +import 'package:monekin/core/presentation/theme.dart'; enum InlineInfoCardMode { warn, info } @@ -23,14 +23,25 @@ class InlineInfoCard extends StatelessWidget { @override Widget build(BuildContext context) { - final Color color = mode == InlineInfoCardMode.info - ? AppColors.of(context).primary - : Colors.amber; + final isDarkBrightness = isAppInDarkBrightness(context); + + final Color bgColor = mode == InlineInfoCardMode.warn + ? Colors.amber.darken(isDarkBrightness ? 0.6 : -0.7) + : Theme.of(context).colorScheme.primaryContainer; + final Color iconColor = mode == InlineInfoCardMode.warn + ? Colors.amber.lighten(isDarkBrightness ? 0.5 : -0.4) + : Theme.of(context).colorScheme.onPrimaryContainer; + + // final iconColor = baseColor.lighten(isDarkBrightness ? 0.5 : -0.4); return Card( - // color: color.withOpacity(0.1), - elevation: 1, + color: bgColor, + elevation: 0, margin: margin, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(width: 2, color: iconColor), + ), child: ResponsiveRowColumn.withSymetricSpacing( spacing: 10, padding: const EdgeInsets.all(8), @@ -41,7 +52,7 @@ class InlineInfoCard extends StatelessWidget { mode == InlineInfoCardMode.info ? Icons.info_rounded : Icons.warning_rounded, - color: color, + color: iconColor, size: 28, ), ), @@ -52,10 +63,9 @@ class InlineInfoCard extends StatelessWidget { textAlign: direction == Axis.vertical ? TextAlign.center : TextAlign.left, - style: const TextStyle( + style: TextStyle( fontSize: 12.25, fontWeight: FontWeight.w400, - //color: Theme.of(context).colorScheme.onPrimary, ), ), ), diff --git a/lib/core/presentation/widgets/no_results.dart b/lib/core/presentation/widgets/no_results.dart index 6e392b74..fc6a4db4 100644 --- a/lib/core/presentation/widgets/no_results.dart +++ b/lib/core/presentation/widgets/no_results.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:monekin/core/presentation/animations/shake/fade_in.dart'; +import 'package:monekin/core/presentation/animations/fade_in.dart'; import 'package:monekin/core/presentation/app_colors.dart'; import 'package:monekin/core/presentation/theme.dart'; @@ -51,7 +51,10 @@ class NoResults extends StatelessWidget { : 'assets/icons/page_states/empty_folder.svg', colorFilter: ColorFilter.mode( tintColor == null - ? AppColors.of(context).primary.withOpacity(0.7) + ? Theme.of(context) + .colorScheme + .primary + .withOpacity(0.7) : tintColor!.withOpacity(0.7), BlendMode.srcIn, ), diff --git a/lib/core/presentation/widgets/tappable.dart b/lib/core/presentation/widgets/tappable.dart index 4fbc7829..a1c5c741 100644 --- a/lib/core/presentation/widgets/tappable.dart +++ b/lib/core/presentation/widgets/tappable.dart @@ -14,7 +14,8 @@ class Tappable extends StatelessWidget { }); final Color? bgColor; - final double? borderRadius; + final BorderRadius? borderRadius; + final ShapeBorder? shape; final EdgeInsets? margin; @@ -31,18 +32,16 @@ class Tappable extends StatelessWidget { margin: margin, child: Material( color: bgColor, - borderRadius: - borderRadius == null ? null : BorderRadius.circular(borderRadius!), + borderRadius: borderRadius, shape: shape, child: InkWell( - onTap: onTap, - onLongPress: onLongPress, - onDoubleTap: onDoubleTap, - customBorder: shape, - borderRadius: borderRadius == null - ? null - : BorderRadius.circular(borderRadius!), - child: child), + onTap: onTap, + onLongPress: onLongPress, + onDoubleTap: onDoubleTap, + customBorder: shape, + borderRadius: borderRadius, + child: child, + ), ), ); } diff --git a/lib/core/presentation/widgets/transaction_filter/filter_sheet_modal.dart b/lib/core/presentation/widgets/transaction_filter/filter_sheet_modal.dart index 75f16df9..2be517f2 100644 --- a/lib/core/presentation/widgets/transaction_filter/filter_sheet_modal.dart +++ b/lib/core/presentation/widgets/transaction_filter/filter_sheet_modal.dart @@ -126,7 +126,8 @@ class _FilterSheetModalState extends State { body: ScrollableWithBottomGradient( controller: scrollController, padding: const EdgeInsets.fromLTRB(16, 2, 16, 24), - gradientColor: AppColors.of(context).modalBackground, + gradientColor: + Theme.of(context).colorSchemeExtended.modalBackground, child: Form( key: _formKey, child: Column( @@ -212,7 +213,7 @@ class _FilterSheetModalState extends State { .map((e) => e == null ? t.tags.without_tags : e.name) .printFormatted() - : t.account.select.all, + : t.tags.select.all, onTap: () => showTagListModal( context, modal: TagSelector( diff --git a/lib/core/presentation/widgets/transaction_filter/tags_filter/tags_filter_container.dart b/lib/core/presentation/widgets/transaction_filter/tags_filter/tags_filter_container.dart deleted file mode 100644 index 66f2a51c..00000000 --- a/lib/core/presentation/widgets/transaction_filter/tags_filter/tags_filter_container.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:monekin/i18n/translations.g.dart'; - -class TagsFilterContainer extends StatelessWidget { - const TagsFilterContainer( - {super.key, - required this.child, - this.headerLabelStyle, - this.headerSpacing = 4}); - - final Widget child; - - final TextStyle? headerLabelStyle; - - /// Space between the header label and the tags chips. Defaults to `4` - final double headerSpacing; - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${t.tags.display(n: 19)}:', - style: headerLabelStyle, - ), - SizedBox(height: headerSpacing), - ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 100, - maxWidth: double.infinity, - minWidth: double.infinity, - ), - child: Card( - elevation: 0, - margin: const EdgeInsets.all(0), - child: SingleChildScrollView( - padding: const EdgeInsets.all(0), - child: child, - ), - )) - ], - ); - } -} diff --git a/lib/core/presentation/widgets/trending_value.dart b/lib/core/presentation/widgets/trending_value.dart index d2ea7907..6c9d3632 100644 --- a/lib/core/presentation/widgets/trending_value.dart +++ b/lib/core/presentation/widgets/trending_value.dart @@ -6,40 +6,45 @@ import 'package:monekin/core/presentation/widgets/number_ui_formatters/ui_number import '../app_colors.dart'; class TrendingValue extends StatelessWidget { - const TrendingValue( - {super.key, - required this.percentage, - this.decimalDigits = 2, - this.fontSize = 14, - this.fontWeight = FontWeight.normal, - this.filled = false, - this.outlined = false}); + const TrendingValue({ + super.key, + required this.percentage, + this.decimalDigits = 2, + this.fontSize = 14, + this.fontWeight = FontWeight.normal, + this.filled = false, + this.outlined = false, + this.markNanAsZero = true, + }); final double percentage; final int decimalDigits; - final double fontSize; - final FontWeight fontWeight; - - final bool filled, outlined; + final bool filled, outlined, markNanAsZero; Widget paintTrendValue(BuildContext context) { - final textColor = - _getColorBasedOnPercentage(context).lighten(filled ? 0.85 : 0); + final textColor = _getColorBasedOnPercentage(context) + .lighten(filled && !outlined ? 0.85 : 0); + + double toDisplay = percentage; + + if (toDisplay.isNaN && markNanAsZero) { + toDisplay = 0; + } return Row( mainAxisSize: MainAxisSize.min, children: [ - if (percentage != 0) + if (toDisplay != 0) Icon( - percentage > 0 + toDisplay > 0 ? Icons.trending_up_rounded : Icons.trending_down_rounded, size: fontSize * (9 / 7), color: textColor, ), - if (percentage == 0) + if (toDisplay == 0) Text( '=', style: TextStyle( @@ -51,7 +56,7 @@ class TrendingValue extends StatelessWidget { ), const SizedBox(width: 6), UINumberFormatter.percentage( - amountToConvert: percentage, + amountToConvert: toDisplay, integerStyle: TextStyle( fontSize: fontSize, fontWeight: fontWeight, @@ -63,10 +68,14 @@ class TrendingValue extends StatelessWidget { } Color _getColorBasedOnPercentage(BuildContext context) { - return percentage == 0 + return (percentage == 0 || percentage.isNaN) ? AppColors.of(context) .brand - .lighten(isAppInDarkBrightness(context) ? 0.45 : 0.25) + .lighten(filled + ? 0 + : isAppInDarkBrightness(context) + ? 0.45 + : 0.25) .withBlue(225) : percentage > 0 ? AppColors.of(context).success @@ -75,22 +84,23 @@ class TrendingValue extends StatelessWidget { @override Widget build(BuildContext context) { - if (filled) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: !filled ? null : (_getColorBasedOnPercentage(context)), - borderRadius: BorderRadius.circular(9999), - border: outlined - ? Border.all( - color: _getColorBasedOnPercentage(context).lighten(0.85), - width: 1) - : null, - ), - child: paintTrendValue(context), - ); - } else { - return paintTrendValue(context); - } + final trendColor = _getColorBasedOnPercentage(context); + final textColor = _getColorBasedOnPercentage(context) + .lighten(filled && !outlined ? 0.85 : 0); + final backgroundColor = filled && outlined + ? trendColor.lighten(0.85).withOpacity(0.95) + : filled + ? trendColor + : null; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(9999), + border: outlined ? Border.all(color: textColor, width: 1) : null, + ), + child: paintTrendValue(context), + ); } } diff --git a/lib/core/presentation/widgets/user_avatar.dart b/lib/core/presentation/widgets/user_avatar.dart index 00c02583..02a786fb 100644 --- a/lib/core/presentation/widgets/user_avatar.dart +++ b/lib/core/presentation/widgets/user_avatar.dart @@ -3,14 +3,19 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:monekin/core/presentation/widgets/skeleton.dart'; class UserAvatar extends StatelessWidget { - const UserAvatar({super.key, this.avatar, this.size = 36, this.border}); + const UserAvatar( + {super.key, + this.avatar, + this.size = 36, + this.border, + this.backgroundColor}); final String? avatar; - final Border? border; - final double size; + final Color? backgroundColor; + @override Widget build(BuildContext context) { final ColorScheme colors = Theme.of(context).colorScheme; @@ -20,14 +25,15 @@ class UserAvatar extends StatelessWidget { padding: const EdgeInsets.all(4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), - color: colors.primaryContainer, + color: backgroundColor ?? colors.primaryContainer, border: border, ), child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: colors.primaryContainer), + borderRadius: BorderRadius.circular(100), + color: backgroundColor ?? colors.primaryContainer, + ), child: Builder(builder: (context) { if (avatar == null) { return const Skeleton(width: 36, height: 36, applyMarging: false); diff --git a/lib/core/routes/destinations.dart b/lib/core/routes/destinations.dart index 1a396177..266567e5 100644 --- a/lib/core/routes/destinations.dart +++ b/lib/core/routes/destinations.dart @@ -34,10 +34,12 @@ class MainMenuDestination { final Widget destination; - NavigationDestination toNavigationDestinationWidget() { + NavigationDestination toNavigationDestinationWidget(BuildContext context) { return NavigationDestination( icon: Icon(icon), - selectedIcon: Icon(selectedIcon ?? icon), + selectedIcon: Icon( + selectedIcon ?? icon, + ), label: label, ); } diff --git a/lib/core/services/supported_icon/getter/supported_icons.dart b/lib/core/services/supported_icon/getter/supported_icons.dart index e7bcf444..5c16b87d 100644 --- a/lib/core/services/supported_icon/getter/supported_icons.dart +++ b/lib/core/services/supported_icon/getter/supported_icons.dart @@ -3,4 +3,4 @@ // To re-generate it, please check the python script under services/utils/icon_downloader /// List of all the supported icons in the app. This list does not take into account app functionality icons, just the icons that are selectable to identificate some accounts, categories... -List> supportedIcons = [{'id': 'pedal_bike', 'scope': 'transport'}, {'id': 'local_shipping', 'scope': 'transport'}, {'id': 'flight', 'scope': 'transport'}, {'id': 'sailing', 'scope': 'transport'}, {'id': 'flight_takeoff', 'scope': 'transport'}, {'id': 'car_rental', 'scope': 'transport'}, {'id': 'car_repair', 'scope': 'transport'}, {'id': 'tram', 'scope': 'transport'}, {'id': 'train', 'scope': 'transport'}, {'id': 'electric_scooter', 'scope': 'transport'}, {'id': 'two_wheeler', 'scope': 'transport'}, {'id': 'local_taxi', 'scope': 'transport'}, {'id': 'payments', 'scope': 'money'}, {'id': 'savings', 'scope': 'money'}, {'id': 'credit_score', 'scope': 'money'}, {'id': 'credit_card', 'scope': 'money'}, {'id': 'redeem', 'scope': 'money'}, {'id': 'account_balance', 'scope': 'money'}, {'id': 'receipt', 'scope': 'money'}, {'id': 'receipt_long', 'scope': 'money'}, {'id': 'auto_graph', 'scope': 'money'}, {'id': 'autopay', 'scope': 'money'}, {'id': 'bank_of_america', 'scope': 'logos/financial_institutions'}, {'id': 'barclays', 'scope': 'logos/financial_institutions'}, {'id': 'paypal', 'scope': 'logos/financial_institutions'}, {'id': 'bbva', 'scope': 'logos/financial_institutions'}, {'id': 'citizens_bank', 'scope': 'logos/financial_institutions'}, {'id': 'deutsche_bank', 'scope': 'logos/financial_institutions'}, {'id': 'hsbc', 'scope': 'logos/financial_institutions'}, {'id': 'jpmorgan', 'scope': 'logos/financial_institutions'}, {'id': 'revolut', 'scope': 'logos/financial_institutions'}, {'id': 'santander', 'scope': 'logos/financial_institutions'}, {'id': 'caixabank', 'scope': 'logos/financial_institutions'}, {'id': 'sabadell', 'scope': 'logos/financial_institutions'}, {'id': 'citibank', 'scope': 'logos/financial_institutions'}, {'id': 'ing', 'scope': 'logos/financial_institutions'}, {'id': 'credit_suisse', 'scope': 'logos/financial_institutions'}, {'id': 'account_balance_wallet', 'scope': 'money'}, {'id': 'wallet', 'scope': 'money'}, {'id': 'kitchen', 'scope': 'food'}, {'id': 'icecream', 'scope': 'food'}, {'id': 'dinner_dining', 'scope': 'food'}, {'id': 'local_dining', 'scope': 'food'}, {'id': 'lunch_dining', 'scope': 'food'}, {'id': 'local_pizza', 'scope': 'food'}, {'id': 'fastfood', 'scope': 'food'}, {'id': 'nutrition', 'scope': 'food'}, {'id': 'dentistry', 'scope': 'medical'}, {'id': 'tibia', 'scope': 'medical'}, {'id': 'diversity_3', 'scope': 'medical'}, {'id': 'cardiology', 'scope': 'medical'}, {'id': 'ecg_heart', 'scope': 'medical'}, {'id': 'fitness_center', 'scope': 'medical'}, {'id': 'monitor_weight', 'scope': 'medical'}, {'id': 'pill', 'scope': 'medical'}, {'id': 'medication', 'scope': 'medical'}, {'id': 'prescriptions', 'scope': 'medical'}, {'id': 'self_improvement', 'scope': 'medical'}, {'id': 'self_care', 'scope': 'medical'}, {'id': 'psychology', 'scope': 'medical'}, {'id': 'sports_soccer', 'scope': 'entertainment'}, {'id': 'confirmation_number', 'scope': 'entertainment'}, {'id': 'pool', 'scope': 'entertainment'}, {'id': 'sports_motorsports', 'scope': 'entertainment'}, {'id': 'sports_baseball', 'scope': 'entertainment'}, {'id': 'sports_basketball', 'scope': 'entertainment'}, {'id': 'sports_cricket', 'scope': 'entertainment'}, {'id': 'golf_course', 'scope': 'entertainment'}, {'id': 'casino', 'scope': 'entertainment'}, {'id': 'roller_skating', 'scope': 'entertainment'}, {'id': 'beach_access', 'scope': 'entertainment'}, {'id': 'paragliding', 'scope': 'entertainment'}, {'id': 'attractions', 'scope': 'entertainment'}, {'id': 'movie', 'scope': 'entertainment'}, {'id': 'museum', 'scope': 'entertainment'}, {'id': 'sports_bar', 'scope': 'entertainment'}, {'id': 'liquor', 'scope': 'entertainment'}, {'id': 'celebration', 'scope': 'entertainment'}, {'id': 'headphones', 'scope': 'technology'}, {'id': 'computer', 'scope': 'technology'}, {'id': 'keyboard', 'scope': 'technology'}, {'id': 'devices', 'scope': 'technology'}, {'id': 'mouse', 'scope': 'technology'}, {'id': 'album', 'scope': 'technology'}, {'id': 'laptop_windows', 'scope': 'technology'}, {'id': 'laptop_mac', 'scope': 'technology'}, {'id': 'stadia_controller', 'scope': 'technology'}, {'id': 'question_mark', 'scope': 'other'}, {'id': 'star', 'scope': 'other'}, {'id': 'swipe_up', 'scope': 'other'}, {'id': 'reply_all', 'scope': 'other'}, {'id': 'apps', 'scope': 'other'}, {'id': 'app_badging', 'scope': 'other'}, {'id': 'bolt', 'scope': 'other'}, {'id': 'speed', 'scope': 'other'}, {'id': 'home', 'scope': 'other'}, {'id': 'home_work', 'scope': 'other'}, {'id': 'work', 'scope': 'other'}, {'id': 'library_books', 'scope': 'other'}, {'id': 'call', 'scope': 'other'}, {'id': 'pets', 'scope': 'other'}, {'id': 'shopping_cart', 'scope': 'other'}, {'id': 'shopping_bag', 'scope': 'other'}, {'id': 'shopping_basket', 'scope': 'other'}, {'id': 'shop', 'scope': 'other'}, {'id': 'iron', 'scope': 'other'}, {'id': 'inventory_2', 'scope': 'other'}, {'id': 'login', 'scope': 'other'}, {'id': 'logout', 'scope': 'other'}, {'id': 'redo', 'scope': 'other'}, {'id': 'undo', 'scope': 'other'}, {'id': 'key', 'scope': 'other'}, {'id': 'music_note', 'scope': 'other'}, {'id': 'store', 'scope': 'other'}, {'id': 'landscape', 'scope': 'other'}, {'id': 'bed', 'scope': 'other'}, {'id': 'language', 'scope': 'other'}, {'id': 'military_tech', 'scope': 'other'}, {'id': 'checkroom', 'scope': 'other'}, {'id': 'sprinkler', 'scope': 'other'}, {'id': 'water', 'scope': 'other'}, {'id': 'water_drop', 'scope': 'other'}]; \ No newline at end of file +List> supportedIcons = [{'id': 'pedal_bike', 'scope': 'transport'}, {'id': 'flight', 'scope': 'transport'}, {'id': 'sailing', 'scope': 'transport'}, {'id': 'transportation', 'scope': 'transport'}, {'id': 'airplane_ticket', 'scope': 'transport'}, {'id': 'local_shipping', 'scope': 'transport'}, {'id': 'connecting_airports', 'scope': 'transport'}, {'id': 'flight_takeoff', 'scope': 'transport'}, {'id': 'car_rental', 'scope': 'transport'}, {'id': 'car_repair', 'scope': 'transport'}, {'id': 'luggage', 'scope': 'transport'}, {'id': 'tram', 'scope': 'transport'}, {'id': 'train', 'scope': 'transport'}, {'id': 'electric_scooter', 'scope': 'transport'}, {'id': 'two_wheeler', 'scope': 'transport'}, {'id': 'local_taxi', 'scope': 'transport'}, {'id': 'payments', 'scope': 'money'}, {'id': 'savings', 'scope': 'money'}, {'id': 'credit_score', 'scope': 'money'}, {'id': 'credit_card', 'scope': 'money'}, {'id': 'redeem', 'scope': 'money'}, {'id': 'account_balance', 'scope': 'money'}, {'id': 'receipt', 'scope': 'money'}, {'id': 'receipt_long', 'scope': 'money'}, {'id': 'auto_graph', 'scope': 'money'}, {'id': 'autopay', 'scope': 'money'}, {'id': 'bank_of_america', 'scope': 'logos/financial_institutions'}, {'id': 'barclays', 'scope': 'logos/financial_institutions'}, {'id': 'paypal', 'scope': 'logos/financial_institutions'}, {'id': 'bbva', 'scope': 'logos/financial_institutions'}, {'id': 'citizens_bank', 'scope': 'logos/financial_institutions'}, {'id': 'deutsche_bank', 'scope': 'logos/financial_institutions'}, {'id': 'hsbc', 'scope': 'logos/financial_institutions'}, {'id': 'jpmorgan', 'scope': 'logos/financial_institutions'}, {'id': 'revolut', 'scope': 'logos/financial_institutions'}, {'id': 'santander', 'scope': 'logos/financial_institutions'}, {'id': 'caixabank', 'scope': 'logos/financial_institutions'}, {'id': 'sabadell', 'scope': 'logos/financial_institutions'}, {'id': 'citibank', 'scope': 'logos/financial_institutions'}, {'id': 'ing', 'scope': 'logos/financial_institutions'}, {'id': 'credit_suisse', 'scope': 'logos/financial_institutions'}, {'id': 'account_balance_wallet', 'scope': 'money'}, {'id': 'wallet', 'scope': 'money'}, {'id': 'kitchen', 'scope': 'food'}, {'id': 'icecream', 'scope': 'food'}, {'id': 'dinner_dining', 'scope': 'food'}, {'id': 'local_dining', 'scope': 'food'}, {'id': 'lunch_dining', 'scope': 'food'}, {'id': 'local_pizza', 'scope': 'food'}, {'id': 'fastfood', 'scope': 'food'}, {'id': 'nutrition', 'scope': 'food'}, {'id': 'dentistry', 'scope': 'medical'}, {'id': 'tibia', 'scope': 'medical'}, {'id': 'diversity_3', 'scope': 'medical'}, {'id': 'cardiology', 'scope': 'medical'}, {'id': 'ecg_heart', 'scope': 'medical'}, {'id': 'fitness_center', 'scope': 'medical'}, {'id': 'monitor_weight', 'scope': 'medical'}, {'id': 'pill', 'scope': 'medical'}, {'id': 'medication', 'scope': 'medical'}, {'id': 'prescriptions', 'scope': 'medical'}, {'id': 'self_improvement', 'scope': 'medical'}, {'id': 'self_care', 'scope': 'medical'}, {'id': 'vaccines', 'scope': 'medical'}, {'id': 'psychology', 'scope': 'medical'}, {'id': 'sports_soccer', 'scope': 'entertainment'}, {'id': 'confirmation_number', 'scope': 'entertainment'}, {'id': 'pool', 'scope': 'entertainment'}, {'id': 'sports_motorsports', 'scope': 'entertainment'}, {'id': 'sports_baseball', 'scope': 'entertainment'}, {'id': 'sports_basketball', 'scope': 'entertainment'}, {'id': 'sports_cricket', 'scope': 'entertainment'}, {'id': 'golf_course', 'scope': 'entertainment'}, {'id': 'casino', 'scope': 'entertainment'}, {'id': 'roller_skating', 'scope': 'entertainment'}, {'id': 'beach_access', 'scope': 'entertainment'}, {'id': 'paragliding', 'scope': 'entertainment'}, {'id': 'attractions', 'scope': 'entertainment'}, {'id': 'movie', 'scope': 'entertainment'}, {'id': 'museum', 'scope': 'entertainment'}, {'id': 'sports_bar', 'scope': 'entertainment'}, {'id': 'liquor', 'scope': 'entertainment'}, {'id': 'celebration', 'scope': 'entertainment'}, {'id': 'headphones', 'scope': 'technology'}, {'id': 'computer', 'scope': 'technology'}, {'id': 'keyboard', 'scope': 'technology'}, {'id': 'devices', 'scope': 'technology'}, {'id': 'phone_iphone', 'scope': 'technology'}, {'id': 'smartphone', 'scope': 'technology'}, {'id': 'power', 'scope': 'technology'}, {'id': 'sd_card', 'scope': 'technology'}, {'id': 'monitor', 'scope': 'technology'}, {'id': 'print', 'scope': 'technology'}, {'id': 'mouse', 'scope': 'technology'}, {'id': 'album', 'scope': 'technology'}, {'id': 'laptop_windows', 'scope': 'technology'}, {'id': 'laptop_mac', 'scope': 'technology'}, {'id': 'stadia_controller', 'scope': 'technology'}, {'id': 'question_mark', 'scope': 'other'}, {'id': 'star', 'scope': 'other'}, {'id': 'swipe_up', 'scope': 'other'}, {'id': 'reply_all', 'scope': 'other'}, {'id': 'apps', 'scope': 'other'}, {'id': 'app_badging', 'scope': 'other'}, {'id': 'bolt', 'scope': 'other'}, {'id': 'speed', 'scope': 'other'}, {'id': 'home', 'scope': 'other'}, {'id': 'home_work', 'scope': 'other'}, {'id': 'work', 'scope': 'other'}, {'id': 'library_books', 'scope': 'other'}, {'id': 'call', 'scope': 'other'}, {'id': 'pets', 'scope': 'other'}, {'id': 'shopping_cart', 'scope': 'other'}, {'id': 'shopping_bag', 'scope': 'other'}, {'id': 'shopping_basket', 'scope': 'other'}, {'id': 'shop', 'scope': 'other'}, {'id': 'iron', 'scope': 'other'}, {'id': 'inventory_2', 'scope': 'other'}, {'id': 'recycling', 'scope': 'other'}, {'id': 'login', 'scope': 'other'}, {'id': 'logout', 'scope': 'other'}, {'id': 'redo', 'scope': 'other'}, {'id': 'undo', 'scope': 'other'}, {'id': 'key', 'scope': 'other'}, {'id': 'music_note', 'scope': 'other'}, {'id': 'store', 'scope': 'other'}, {'id': 'landscape', 'scope': 'other'}, {'id': 'smoking_rooms', 'scope': 'other'}, {'id': 'bed', 'scope': 'other'}, {'id': 'language', 'scope': 'other'}, {'id': 'content_cut', 'scope': 'other'}, {'id': 'military_tech', 'scope': 'other'}, {'id': 'checkroom', 'scope': 'other'}, {'id': 'sprinkler', 'scope': 'other'}, {'id': 'water', 'scope': 'other'}, {'id': 'water_drop', 'scope': 'other'}]; \ No newline at end of file diff --git a/lib/core/services/supported_icon/getter/supported_icons.json b/lib/core/services/supported_icon/getter/supported_icons.json index 7596f406..8ac3fbda 100644 --- a/lib/core/services/supported_icon/getter/supported_icons.json +++ b/lib/core/services/supported_icon/getter/supported_icons.json @@ -1,11 +1,15 @@ [ { "id": "pedal_bike", "scope": "transport" }, - { "id": "local_shipping", "scope": "transport" }, { "id": "flight", "scope": "transport" }, { "id": "sailing", "scope": "transport" }, + { "id": "transportation", "scope": "transport" }, + { "id": "airplane_ticket", "scope": "transport" }, + { "id": "local_shipping", "scope": "transport" }, + { "id": "connecting_airports", "scope": "transport" }, { "id": "flight_takeoff", "scope": "transport" }, { "id": "car_rental", "scope": "transport" }, { "id": "car_repair", "scope": "transport" }, + { "id": "luggage", "scope": "transport" }, { "id": "tram", "scope": "transport" }, { "id": "train", "scope": "transport" }, { "id": "electric_scooter", "scope": "transport" }, @@ -58,6 +62,7 @@ { "id": "prescriptions", "scope": "medical" }, { "id": "self_improvement", "scope": "medical" }, { "id": "self_care", "scope": "medical" }, + { "id": "vaccines", "scope": "medical" }, { "id": "psychology", "scope": "medical" }, { "id": "sports_soccer", "scope": "entertainment" }, { "id": "confirmation_number", "scope": "entertainment" }, @@ -81,6 +86,12 @@ { "id": "computer", "scope": "technology" }, { "id": "keyboard", "scope": "technology" }, { "id": "devices", "scope": "technology" }, + { "id": "phone_iphone", "scope": "technology" }, + { "id": "smartphone", "scope": "technology" }, + { "id": "power", "scope": "technology" }, + { "id": "sd_card", "scope": "technology" }, + { "id": "monitor", "scope": "technology" }, + { "id": "print", "scope": "technology" }, { "id": "mouse", "scope": "technology" }, { "id": "album", "scope": "technology" }, { "id": "laptop_windows", "scope": "technology" }, @@ -106,6 +117,7 @@ { "id": "shop", "scope": "other" }, { "id": "iron", "scope": "other" }, { "id": "inventory_2", "scope": "other" }, + { "id": "recycling", "scope": "other" }, { "id": "login", "scope": "other" }, { "id": "logout", "scope": "other" }, { "id": "redo", "scope": "other" }, @@ -114,8 +126,10 @@ { "id": "music_note", "scope": "other" }, { "id": "store", "scope": "other" }, { "id": "landscape", "scope": "other" }, + { "id": "smoking_rooms", "scope": "other" }, { "id": "bed", "scope": "other" }, { "id": "language", "scope": "other" }, + { "id": "content_cut", "scope": "other" }, { "id": "military_tech", "scope": "other" }, { "id": "checkroom", "scope": "other" }, { "id": "sprinkler", "scope": "other" }, diff --git a/lib/core/utils/focus.dart b/lib/core/utils/focus.dart new file mode 100644 index 00000000..6c3dfb41 --- /dev/null +++ b/lib/core/utils/focus.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +unfocusCurrentFocusedItem(BuildContext context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } +} diff --git a/lib/core/utils/list_tile_action_item.dart b/lib/core/utils/list_tile_action_item.dart index f01e4338..2e70d142 100644 --- a/lib/core/utils/list_tile_action_item.dart +++ b/lib/core/utils/list_tile_action_item.dart @@ -28,6 +28,6 @@ class ListTileActionItem { } } - return AppColors.of(context).primary; + return Theme.of(context).colorScheme.primary; } } diff --git a/lib/i18n/strings_en.json b/lib/i18n/strings_en.json index 209aaa70..599130a2 100644 --- a/lib/i18n/strings_en.json +++ b/lib/i18n/strings_en.json @@ -21,6 +21,7 @@ "today": "Today", "yesterday": "Yesterday", "filters": "Filters", + "see-more": "See more", "select-all": "Select all", "deselect-all": "Deselect all", "empty-warn": "Ops! This is very empty", @@ -125,6 +126,7 @@ "Date": "By date" }, "VALIDATIONS": { + "form_error": "Fix the indicated fields to continue", "required": "Required field", "positive": "Should be positive", "min_number": "Should be greater than {{x}}", @@ -213,10 +215,14 @@ "balance": "Balance", "final-balance": "Final balance", "balance-by-account": "Balance by accounts", + "balance-by-account-subtitle": "Where do I have most of my money?", "balance-by-currency": "Balance by currency", - "cash-flow": "Cash flow", - "balance-evolution": "Balance evolution", + "balance-by-currency-subtitle": "How much money do I have in foreign currency?", + "balance-evolution": "Balance trend", + "balance-evolution-subtitle": "Do I have more money than before?", "compared-to-previous-period": "Compared to the previous period", + "cash-flow": "Cash flow", + "cash-flow-subtitle": "Am I spending less than I earn?", "by-periods": "By periods", "by-categories": "By categories", "by-tags": "By tags", @@ -281,7 +287,7 @@ "recurrent-rule-finished": "The recurring rule has been completed, there are no more payments to make!" }, "LIST": { - "empty": "No transactions found to display here. Add a transaction by clicking the '+' button at the bottom", + "empty": "No transactions found to display here. Add a few transactions in the app and maybe you'll have better luck next time.", "searcher.placeholder": "Search by category, description...", "searcher.no-results": "No transactions found matching the search criteria", "loading": "Loading more transactions...", @@ -483,9 +489,12 @@ "name": "Tag name", "description": "Description" }, + "SELECT": { + "title": "Select tags", + "all": "All the tags" + }, "empty-list": "You haven't created any tags yet. Tags and categories are a great way to categorize your movements", "without-tags": "Without tags", - "select": "Select tags", "add": "Add tag", "create": "Create label", "create.success": "Label created successfully", diff --git a/lib/i18n/strings_es.json b/lib/i18n/strings_es.json index 617b55cc..0abfccc7 100644 --- a/lib/i18n/strings_es.json +++ b/lib/i18n/strings_es.json @@ -21,6 +21,7 @@ "today": "Hoy", "yesterday": "Ayer", "filters": "Filtros", + "see-more": "Ver más", "select-all": "Seleccionar todo", "deselect-all": "Deseleccionar todo", "empty-warn": "Ops! Esto esta muy vacio", @@ -126,6 +127,7 @@ "Date": "Por fecha" }, "VALIDATIONS": { + "form_error": "Corrije los campos indicados en el formulario para continuar", "required": "Campo obligatorio", "positive": "Debe ser positivo", "min_number": "Debe ser mayor que {{x}}", @@ -153,7 +155,7 @@ "lastSlideDescr2": "Esperemos que disfrutes de tu experiencia! No dudes en contactar con nosotros en caso de dudas, sugerencias..." }, "HOME": { - "title": "Dashboard", + "title": "Inicio", "filter-transactions": "Filtrar transacciones", "hello-day": "Buenos días,", "hello-night": "Buenas noches,", @@ -217,10 +219,14 @@ "balance": "Saldo", "final-balance": "Saldo final", "balance-by-account": "Saldo por cuentas", + "balance-by-account-subtitle": "¿Donde tengo la mayor parte de mi dinero?", "balance-by-currency": "Saldo por divisas", + "balance-by-currency-subtitle": "¿Cuanto dinero tengo en moneda extranjera?", "balance-evolution": "Tendencia de saldo", + "balance-evolution-subtitle": "¿Tengo más dinero que antes?", "compared-to-previous-period": "Frente al periodo anterior", "cash-flow": "Flujo de caja", + "cash-flow-subtitle": "¿Estoy gastando menos de lo que gano?", "by-periods": "Por periodos", "by-categories": "Por categorías", "by-tags": "Por etiquetas", @@ -285,7 +291,7 @@ "recurrent-rule-finished": "La regla recurrente se ha completado, ya no hay mas pagos a realizar!" }, "LIST": { - "empty": "No se han encontrado transacciones que mostrar aquí. Añade una transacción pulsando el botón '+' de la parte inferior", + "empty": "No se han encontrado transacciones que mostrar aquí. Añade unas cuantas transacciones en la app y quizas tengas más suerte la proxima vez", "searcher.placeholder": "Busca por categoría, descripción...", "searcher.no-results": "No se han encontrado transacciones que coincidan con los criterios de busqueda", "loading": "Cargando más transacciones...", @@ -490,9 +496,12 @@ "name": "Nombre de la etiqueta", "description": "Descripción" }, + "SELECT": { + "title": "Selecciona etiquetas", + "all": "Todas las etiquetas" + }, "empty-list": "No has creado ninguna etiqueta aun. Las etiquetas y las categorías son una gran forma de categorizar tus movimientos", "without-tags": "Sin etiquetas", - "select": "Selecionar etiquetas", "create": "Crear etiqueta", "add": "Añadir etiqueta", "create.success": "Etiqueta creada correctamente", diff --git a/lib/i18n/strings_uk.json b/lib/i18n/strings_uk.json index aaf6500e..966c18f6 100644 --- a/lib/i18n/strings_uk.json +++ b/lib/i18n/strings_uk.json @@ -21,6 +21,7 @@ "today": "Сьогодні", "yesterday": "Вчора", "filters": "Фільтри", + "see-more": "Побачити більше", "select-all": "Вибрати всі", "deselect-all": "Скасувати вибір усіх", "empty-warn": "Ой! Тут порожньо", @@ -125,6 +126,7 @@ "Date": "За датою" }, "VALIDATIONS": { + "form_error": "Виправте поля, зазначені у формі, щоб продовжити", "required": "Обов'язкове поле", "positive": "Повинно бути позитивним", "min_number": "Повинно бути більшим, ніж {{x}}", @@ -212,10 +214,14 @@ "balance": "Баланс", "final-balance": "Кінцевий баланс", "balance-by-account": "Баланс за рахунками", - "balance-by-currency": "Баланс за валютами", - "cash-flow": "Грошовий потік", - "balance-evolution": "Еволюція балансу", + "balance-by-account-subtitle": "Де я маю більшість грошей?", + "balance-by-currency": "Баланс за валютою", + "balance-by-currency-subtitle": "Скільки я маю грошей в іноземній валюті?", + "balance-evolution": "Тенденція балансу", + "balance-evolution-subtitle": "У мене більше грошей, ніж раніше?", "compared-to-previous-period": "Порівняно з попереднім періодом", + "cash-flow": "Грошовий потік", + "cash-flow-subtitle": "Я витрачаю менше, ніж заробляю?", "by-periods": "За періодами", "by-categories": "За категоріями", "by-tags": "За тегами", @@ -280,7 +286,7 @@ "recurrent-rule-finished": "Правило періодичності було завершено, більше немає платежів!" }, "LIST": { - "empty": "Тут не знайдено жодних транзакцій для відображення. Додайте транзакцію, натиснувши кнопку '+' внизу", + "empty": "Тут не знайдено жодних транзакцій для відображення. Додайте кілька транзакцій у додаток, і, можливо, наступного разу вам пощастить більше", "searcher.placeholder": "Шукати за категорією, описом...", "searcher.no-results": "Не знайдено транзакцій, що відповідають критеріям пошуку", "loading": "Завантаження додаткових транзакцій...", @@ -485,9 +491,12 @@ "name": "Назва тегу", "description": "Опис" }, + "SELECT": { + "title": "Вибрати теги", + "all": "Усі теги" + }, "empty-list": "Ви ще не створили жодних тегів. Теги та категорії - це відмінний спосіб категоризувати ваші рухи", "without-tags": "Без тегів", - "select": "Вибрати теги", "add": "Додати тег", "create": "Створити мітку", "create.success": "Мітка успішно створена", diff --git a/lib/i18n/strings_zh-TW.json b/lib/i18n/strings_zh-TW.json index 5959606a..d7a02b66 100644 --- a/lib/i18n/strings_zh-TW.json +++ b/lib/i18n/strings_zh-TW.json @@ -21,6 +21,7 @@ "today": "今天", "yesterday": "昨天", "filters": "過濾器", + "see-more": "查看更多", "select-all": "全選", "deselect-all": "取消全選", "empty-warn": "哦!這裡非常空", @@ -125,6 +126,7 @@ "Date": "按日期" }, "VALIDATIONS": { + "form_error": "修正表單中指示的欄位以繼續", "required": "必填項目", "positive": "應該是積極的", "min_number": "應該大於{{x}}", @@ -213,10 +215,14 @@ "balance": "平衡", "final-balance": "最終餘額", "balance-by-account": "帳戶餘額", - "balance-by-currency": "按貨幣劃分的餘額", - "cash-flow": "現金週轉", - "balance-evolution": "Balance evolution", - "compared-to-previous-period": "與前期相比", + "balance-by-account-subtitle": "我的大部分钱都在哪里?", + "balance-by-currency": "按货币余额", + "balance-by-currency-subtitle": "我有多少钱的外币?", + "balance-evolution": "平衡趋势", + "balance-evolution-subtitle": "我的钱比以前多了吗?", + "compared-to-previous-period": "与上一时期相比", + "cash-flow": "现金流", + "cash-flow-subtitle": "我的支出是否少于我的收入?", "by-periods": "按時期", "by-categories": "按類別", "by-tags": "按標籤", @@ -281,7 +287,7 @@ "recurrent-rule-finished": "循環規則已完成,無需再支付!" }, "LIST": { - "empty": "未發現此處顯示的交易。請點選底部的 '+' 按鈕新增交易", + "empty": "未發現此處顯示的交易。在應用程式中添加一些交易,也許您下次會有更好的運氣", "searcher.placeholder": "按類別、描述搜尋...", "searcher.no-results": "未找到符合搜尋條件的交易", "loading": "正在加載更多交易...", @@ -483,9 +489,12 @@ "name": "標籤名", "description": "描述" }, + "SELECT": { + "title": "選擇標籤", + "all": "所有標籤" + }, "empty-list": "您還沒有創建任何標籤。標籤和類別是對您的動作進行分類的好方法", "without-tags": "沒有標籤", - "select": "選擇標籤", "add": "添加標籤", "create": "建立標籤", "create.success": "標籤創建成功", diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index 08703d09..72243a54 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -4,10 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 4 -/// Strings: 2133 (533 per locale) +/// Strings: 2189 (547 per locale) /// -/// Built on 2024-08-07 at 15:39 UTC - +/// Built on 2024-10-02 at 22:26 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -205,6 +204,7 @@ class _TranslationsGeneralEn { String get today => 'Today'; String get yesterday => 'Yesterday'; String get filters => 'Filters'; + String get see_more => 'See more'; String get select_all => 'Select all'; String get deselect_all => 'Deselect all'; String get empty_warn => 'Ops! This is very empty'; @@ -290,10 +290,14 @@ class _TranslationsStatsEn { String get balance => 'Balance'; String get final_balance => 'Final balance'; String get balance_by_account => 'Balance by accounts'; + String get balance_by_account_subtitle => 'Where do I have most of my money?'; String get balance_by_currency => 'Balance by currency'; - String get cash_flow => 'Cash flow'; - String get balance_evolution => 'Balance evolution'; + String get balance_by_currency_subtitle => 'How much money do I have in foreign currency?'; + String get balance_evolution => 'Balance trend'; + String get balance_evolution_subtitle => 'Do I have more money than before?'; String get compared_to_previous_period => 'Compared to the previous period'; + String get cash_flow => 'Cash flow'; + String get cash_flow_subtitle => 'Am I spending less than I earn?'; String get by_periods => 'By periods'; String get by_categories => 'By categories'; String get by_tags => 'By tags'; @@ -450,9 +454,9 @@ class _TranslationsTagsEn { other: 'Tags', ); late final _TranslationsTagsFormEn form = _TranslationsTagsFormEn._(_root); + late final _TranslationsTagsSelectEn select = _TranslationsTagsSelectEn._(_root); String get empty_list => 'You haven\'t created any tags yet. Tags and categories are a great way to categorize your movements'; String get without_tags => 'Without tags'; - String get select => 'Select tags'; String get add => 'Add tag'; String get create => 'Create label'; String get create_success => 'Label created successfully'; @@ -636,6 +640,7 @@ class _TranslationsGeneralValidationsEn { final Translations _root; // ignore: unused_field // Translations + String get form_error => 'Fix the indicated fields to continue'; String get required => 'Required field'; String get positive => 'Should be positive'; String min_number({required Object x}) => 'Should be greater than ${x}'; @@ -774,7 +779,7 @@ class _TranslationsTransactionListEn { final Translations _root; // ignore: unused_field // Translations - String get empty => 'No transactions found to display here. Add a transaction by clicking the \'+\' button at the bottom'; + String get empty => 'No transactions found to display here. Add a few transactions in the app and maybe you\'ll have better luck next time.'; String get searcher_placeholder => 'Search by category, description...'; String get searcher_no_results => 'No transactions found matching the search criteria'; String get loading => 'Loading more transactions...'; @@ -1015,6 +1020,17 @@ class _TranslationsTagsFormEn { String get description => 'Description'; } +// Path: tags.select +class _TranslationsTagsSelectEn { + _TranslationsTagsSelectEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Select tags'; + String get all => 'All the tags'; +} + // Path: categories.select class _TranslationsCategoriesSelectEn { _TranslationsCategoriesSelectEn._(this._root); @@ -1487,6 +1503,7 @@ class _TranslationsGeneralEs implements _TranslationsGeneralEn { @override String get today => 'Hoy'; @override String get yesterday => 'Ayer'; @override String get filters => 'Filtros'; + @override String get see_more => 'Ver más'; @override String get select_all => 'Seleccionar todo'; @override String get deselect_all => 'Deseleccionar todo'; @override String get empty_warn => 'Ops! Esto esta muy vacio'; @@ -1534,7 +1551,7 @@ class _TranslationsHomeEs implements _TranslationsHomeEn { @override final _TranslationsEs _root; // ignore: unused_field // Translations - @override String get title => 'Dashboard'; + @override String get title => 'Inicio'; @override String get filter_transactions => 'Filtrar transacciones'; @override String get hello_day => 'Buenos días,'; @override String get hello_night => 'Buenas noches,'; @@ -1572,10 +1589,14 @@ class _TranslationsStatsEs implements _TranslationsStatsEn { @override String get balance => 'Saldo'; @override String get final_balance => 'Saldo final'; @override String get balance_by_account => 'Saldo por cuentas'; + @override String get balance_by_account_subtitle => '¿Donde tengo la mayor parte de mi dinero?'; @override String get balance_by_currency => 'Saldo por divisas'; + @override String get balance_by_currency_subtitle => '¿Cuanto dinero tengo en moneda extranjera?'; @override String get balance_evolution => 'Tendencia de saldo'; + @override String get balance_evolution_subtitle => '¿Tengo más dinero que antes?'; @override String get compared_to_previous_period => 'Frente al periodo anterior'; @override String get cash_flow => 'Flujo de caja'; + @override String get cash_flow_subtitle => '¿Estoy gastando menos de lo que gano?'; @override String get by_periods => 'Por periodos'; @override String get by_categories => 'Por categorías'; @override String get by_tags => 'Por etiquetas'; @@ -1732,9 +1753,9 @@ class _TranslationsTagsEs implements _TranslationsTagsEn { other: 'Etiquetas', ); @override late final _TranslationsTagsFormEs form = _TranslationsTagsFormEs._(_root); + @override late final _TranslationsTagsSelectEs select = _TranslationsTagsSelectEs._(_root); @override String get empty_list => 'No has creado ninguna etiqueta aun. Las etiquetas y las categorías son una gran forma de categorizar tus movimientos'; @override String get without_tags => 'Sin etiquetas'; - @override String get select => 'Selecionar etiquetas'; @override String get create => 'Crear etiqueta'; @override String get add => 'Añadir etiqueta'; @override String get create_success => 'Etiqueta creada correctamente'; @@ -1918,6 +1939,7 @@ class _TranslationsGeneralValidationsEs implements _TranslationsGeneralValidatio @override final _TranslationsEs _root; // ignore: unused_field // Translations + @override String get form_error => 'Corrije los campos indicados en el formulario para continuar'; @override String get required => 'Campo obligatorio'; @override String get positive => 'Debe ser positivo'; @override String min_number({required Object x}) => 'Debe ser mayor que ${x}'; @@ -2056,7 +2078,7 @@ class _TranslationsTransactionListEs implements _TranslationsTransactionListEn { @override final _TranslationsEs _root; // ignore: unused_field // Translations - @override String get empty => 'No se han encontrado transacciones que mostrar aquí. Añade una transacción pulsando el botón \'+\' de la parte inferior'; + @override String get empty => 'No se han encontrado transacciones que mostrar aquí. Añade unas cuantas transacciones en la app y quizas tengas más suerte la proxima vez'; @override String get searcher_placeholder => 'Busca por categoría, descripción...'; @override String get searcher_no_results => 'No se han encontrado transacciones que coincidan con los criterios de busqueda'; @override String get loading => 'Cargando más transacciones...'; @@ -2297,6 +2319,17 @@ class _TranslationsTagsFormEs implements _TranslationsTagsFormEn { @override String get description => 'Descripción'; } +// Path: tags.select +class _TranslationsTagsSelectEs implements _TranslationsTagsSelectEn { + _TranslationsTagsSelectEs._(this._root); + + @override final _TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Selecciona etiquetas'; + @override String get all => 'Todas las etiquetas'; +} + // Path: categories.select class _TranslationsCategoriesSelectEs implements _TranslationsCategoriesSelectEn { _TranslationsCategoriesSelectEs._(this._root); @@ -2770,6 +2803,7 @@ class _TranslationsGeneralUk implements _TranslationsGeneralEn { @override String get today => 'Сьогодні'; @override String get yesterday => 'Вчора'; @override String get filters => 'Фільтри'; + @override String get see_more => 'Побачити більше'; @override String get select_all => 'Вибрати всі'; @override String get deselect_all => 'Скасувати вибір усіх'; @override String get empty_warn => 'Ой! Тут порожньо'; @@ -2855,10 +2889,14 @@ class _TranslationsStatsUk implements _TranslationsStatsEn { @override String get balance => 'Баланс'; @override String get final_balance => 'Кінцевий баланс'; @override String get balance_by_account => 'Баланс за рахунками'; - @override String get balance_by_currency => 'Баланс за валютами'; - @override String get cash_flow => 'Грошовий потік'; - @override String get balance_evolution => 'Еволюція балансу'; + @override String get balance_by_account_subtitle => 'Де я маю більшість грошей?'; + @override String get balance_by_currency => 'Баланс за валютою'; + @override String get balance_by_currency_subtitle => 'Скільки я маю грошей в іноземній валюті?'; + @override String get balance_evolution => 'Тенденція балансу'; + @override String get balance_evolution_subtitle => 'У мене більше грошей, ніж раніше?'; @override String get compared_to_previous_period => 'Порівняно з попереднім періодом'; + @override String get cash_flow => 'Грошовий потік'; + @override String get cash_flow_subtitle => 'Я витрачаю менше, ніж заробляю?'; @override String get by_periods => 'За періодами'; @override String get by_categories => 'За категоріями'; @override String get by_tags => 'За тегами'; @@ -3015,9 +3053,9 @@ class _TranslationsTagsUk implements _TranslationsTagsEn { other: 'Теги', ); @override late final _TranslationsTagsFormUk form = _TranslationsTagsFormUk._(_root); + @override late final _TranslationsTagsSelectUk select = _TranslationsTagsSelectUk._(_root); @override String get empty_list => 'Ви ще не створили жодних тегів. Теги та категорії - це відмінний спосіб категоризувати ваші рухи'; @override String get without_tags => 'Без тегів'; - @override String get select => 'Вибрати теги'; @override String get add => 'Додати тег'; @override String get create => 'Створити мітку'; @override String get create_success => 'Мітка успішно створена'; @@ -3201,6 +3239,7 @@ class _TranslationsGeneralValidationsUk implements _TranslationsGeneralValidatio @override final _TranslationsUk _root; // ignore: unused_field // Translations + @override String get form_error => 'Виправте поля, зазначені у формі, щоб продовжити'; @override String get required => 'Обов\'язкове поле'; @override String get positive => 'Повинно бути позитивним'; @override String min_number({required Object x}) => 'Повинно бути більшим, ніж ${x}'; @@ -3339,7 +3378,7 @@ class _TranslationsTransactionListUk implements _TranslationsTransactionListEn { @override final _TranslationsUk _root; // ignore: unused_field // Translations - @override String get empty => 'Тут не знайдено жодних транзакцій для відображення. Додайте транзакцію, натиснувши кнопку \'+\' внизу'; + @override String get empty => 'Тут не знайдено жодних транзакцій для відображення. Додайте кілька транзакцій у додаток, і, можливо, наступного разу вам пощастить більше'; @override String get searcher_placeholder => 'Шукати за категорією, описом...'; @override String get searcher_no_results => 'Не знайдено транзакцій, що відповідають критеріям пошуку'; @override String get loading => 'Завантаження додаткових транзакцій...'; @@ -3580,6 +3619,17 @@ class _TranslationsTagsFormUk implements _TranslationsTagsFormEn { @override String get description => 'Опис'; } +// Path: tags.select +class _TranslationsTagsSelectUk implements _TranslationsTagsSelectEn { + _TranslationsTagsSelectUk._(this._root); + + @override final _TranslationsUk _root; // ignore: unused_field + + // Translations + @override String get title => 'Вибрати теги'; + @override String get all => 'Усі теги'; +} + // Path: categories.select class _TranslationsCategoriesSelectUk implements _TranslationsCategoriesSelectEn { _TranslationsCategoriesSelectUk._(this._root); @@ -4038,7 +4088,7 @@ class _TranslationsGeneralZhTw implements _TranslationsGeneralEn { @override String get confirm => '確認'; @override String get continue_text => '繼續'; @override String get quick_actions => '快速行動'; - @override String get save => '節省'; + @override String get save => '保存'; @override String get save_changes => '儲存變更'; @override String get close_and_save => '儲存並關閉'; @override String get add => '添加'; @@ -4052,6 +4102,7 @@ class _TranslationsGeneralZhTw implements _TranslationsGeneralEn { @override String get today => '今天'; @override String get yesterday => '昨天'; @override String get filters => '過濾器'; + @override String get see_more => '查看更多'; @override String get select_all => '全選'; @override String get deselect_all => '取消全選'; @override String get empty_warn => '哦!這裡非常空'; @@ -4137,10 +4188,14 @@ class _TranslationsStatsZhTw implements _TranslationsStatsEn { @override String get balance => '平衡'; @override String get final_balance => '最終餘額'; @override String get balance_by_account => '帳戶餘額'; - @override String get balance_by_currency => '按貨幣劃分的餘額'; - @override String get cash_flow => '現金週轉'; - @override String get balance_evolution => 'Balance evolution'; - @override String get compared_to_previous_period => '與前期相比'; + @override String get balance_by_account_subtitle => '我的大部分钱都在哪里?'; + @override String get balance_by_currency => '按货币余额'; + @override String get balance_by_currency_subtitle => '我有多少钱的外币?'; + @override String get balance_evolution => '平衡趋势'; + @override String get balance_evolution_subtitle => '我的钱比以前多了吗?'; + @override String get compared_to_previous_period => '与上一时期相比'; + @override String get cash_flow => '现金流'; + @override String get cash_flow_subtitle => '我的支出是否少于我的收入?'; @override String get by_periods => '按時期'; @override String get by_categories => '按類別'; @override String get by_tags => '按標籤'; @@ -4184,7 +4239,7 @@ class _TranslationsTransactionZhTw implements _TranslationsTransactionEn { @override String get edit => '編輯交易'; @override String get edit_success => '交易編輯成功'; @override String get edit_multiple => '編輯交易'; - @override String edit_multiple_success({required Object x}) => '${x} 筆交易已成功編輯'; + @override String edit_multiple_success({required Object x}) => '${x}筆交易已成功編輯'; @override String get duplicate => '克隆交易'; @override String get duplicate_short => '複製'; @override String get duplicate_warning_message => '將在同一日期創建與此相同的交易,您想繼續嗎?'; @@ -4193,8 +4248,8 @@ class _TranslationsTransactionZhTw implements _TranslationsTransactionEn { @override String get delete_warning_message => '此操作不可逆轉。您的帳戶當前餘額和所有統計資料都將重新計算'; @override String get delete_success => '交易已正確刪除'; @override String get delete_multiple => '刪除交易'; - @override String delete_multiple_warning_message({required Object x}) => '此操作不可逆轉,將刪除 ${x} 筆交易。您帳戶的當前餘額和所有統計資料都將重新計算'; - @override String delete_multiple_success({required Object x}) => '正確刪除了 ${x} 筆交易'; + @override String delete_multiple_warning_message({required Object x}) => '此操作不可逆轉,將刪除${x}筆交易。您帳戶的當前餘額和所有統計資料都將重新計算'; + @override String delete_multiple_success({required Object x}) => '正確刪除了${x}筆交易'; @override String get details => '動作詳情'; @override late final _TranslationsTransactionNextPaymentsZhTw next_payments = _TranslationsTransactionNextPaymentsZhTw._(_root); @override late final _TranslationsTransactionListZhTw list = _TranslationsTransactionListZhTw._(_root); @@ -4213,9 +4268,9 @@ class _TranslationsTransferZhTw implements _TranslationsTransferEn { // Translations @override String get display => '轉移'; - @override String get transfers => '轉帳'; - @override String transfer_to({required Object account}) => '轉帳至${account}'; - @override String get create => '新轉移'; + @override String get transfers => '轉移'; + @override String transfer_to({required Object account}) => '轉移至${account}'; + @override String get create => '轉移'; @override String get need_two_accounts_warning_header => 'Ops!'; @override String get need_two_accounts_warning_message => '至少需要兩個帳戶才能執行此操作。如果您需要調整或編輯該帳戶的當前餘額,請點擊編輯按鈕'; @override late final _TranslationsTransferFormZhTw form = _TranslationsTransferFormZhTw._(_root); @@ -4231,7 +4286,7 @@ class _TranslationsRecurrentTransactionsZhTw implements _TranslationsRecurrentTr @override String get title => '經常性交易'; @override String get title_short => '記錄交易'; @override String get empty => '您似乎沒有任何經常性交易。創建每月、每年或每週的經常性交易,它將顯示在此處'; - @override String get total_expense_title => '每個期間的總費用'; + @override String get total_expense_title => '每個期間的總收入'; @override String get total_expense_descr => '*不考慮每次重複的開始和結束日期'; @override late final _TranslationsRecurrentTransactionsDetailsZhTw details = _TranslationsRecurrentTransactionsDetailsZhTw._(_root); } @@ -4251,8 +4306,8 @@ class _TranslationsAccountZhTw implements _TranslationsAccountEn { @override String get reopen_descr => '您確定要重新開啟此帳戶嗎?'; @override String get balance => '帳戶餘額'; @override String get n_transactions => '交易數量'; - @override String get add_money => '加錢'; - @override String get withdraw_money => '取錢'; + @override String get add_money => '增加金額'; + @override String get withdraw_money => '取出金額'; @override String get no_accounts => '未發現此處顯示的交易。請點選底部的 \'+\' 按鈕新增交易'; @override late final _TranslationsAccountTypesZhTw types = _TranslationsAccountTypesZhTw._(_root); @override late final _TranslationsAccountFormZhTw form = _TranslationsAccountFormZhTw._(_root); @@ -4270,7 +4325,7 @@ class _TranslationsCurrenciesZhTw implements _TranslationsCurrenciesEn { // Translations @override String get currency_converter => '貨幣換算'; @override String get currency => '貨幣'; - @override String get currency_manager => '貨幣經理'; + @override String get currency_manager => '貨幣管理'; @override String get currency_manager_descr => '配置您的貨幣及其與其他貨幣的匯率'; @override String get preferred_currency => '首選/基礎貨幣'; @override String get change_preferred_currency_title => '更改首選貨幣'; @@ -4293,13 +4348,13 @@ class _TranslationsTagsZhTw implements _TranslationsTagsEn { // Translations @override String display({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: 'one', - other: 'other', + one: '標籤', + other: '標籤', ); @override late final _TranslationsTagsFormZhTw form = _TranslationsTagsFormZhTw._(_root); + @override late final _TranslationsTagsSelectZhTw select = _TranslationsTagsSelectZhTw._(_root); @override String get empty_list => '您還沒有創建任何標籤。標籤和類別是對您的動作進行分類的好方法'; @override String get without_tags => '沒有標籤'; - @override String get select => '選擇標籤'; @override String get add => '添加標籤'; @override String get create => '建立標籤'; @override String get create_success => '標籤創建成功'; @@ -4322,7 +4377,7 @@ class _TranslationsCategoriesZhTw implements _TranslationsCategoriesEn { @override String get create => '創建類別'; @override String get create_success => '類別創建正確'; @override String get new_category => '新類別'; - @override String get already_exists => '該類別的名稱已存在。也許您想編輯它'; + @override String get already_exists => '該類別的名稱已存在。也許您可以編輯'; @override String get edit => '編輯類別'; @override String get edit_success => '類別編輯正確'; @override String get name => '分類名稱'; @@ -4336,7 +4391,7 @@ class _TranslationsCategoriesZhTw implements _TranslationsCategoriesEn { @override String make_child_warning2({required Object x, required Object destiny}) => '他們的交易(${x})將被移至${destiny}類別中創建的新子類別。'; @override String get make_child_success => '子類別創建成功'; @override String get merge => '與另一個類別合併'; - @override String merge_warning1({required Object from, required Object x, required Object destiny}) => '與類別 ${from} 相關的所有交易 (${x}) 將移至類別 ${destiny}'; + @override String merge_warning1({required Object from, required Object x, required Object destiny}) => '與類別${from}相關的所有交易 (${x}) 將移至類別${destiny}'; @override String merge_warning2({required Object from}) => '類別${from}將被不可逆轉地刪除。 '; @override String get merge_success => '類別合併成功'; @override String get delete_success => '類別已正確刪除'; @@ -4352,16 +4407,16 @@ class _TranslationsBudgetsZhTw implements _TranslationsBudgetsEn { @override final _TranslationsZhTw _root; // ignore: unused_field // Translations - @override String get title => 'title'; + @override String get title => '預算'; @override String get repeated => '再次發生的'; @override String get one_time => '一次'; - @override String get annual => 'annual'; + @override String get annual => '年度'; @override String get week => '每週'; @override String get month => '每月'; @override String get actives => '活躍'; @override String get pending => '等待開始'; @override String get finish => '完成的'; - @override String get from_budgeted => '的左邊 '; + @override String get from_budgeted => '從預算'; @override String get days_left => '還剩幾天'; @override String get days_to_start => '開始的日子'; @override String get since_expiration => '自到期日起的天數'; @@ -4405,8 +4460,8 @@ class _TranslationsSettingsZhTw implements _TranslationsSettingsEn { @override String get theme_and_colors => '主題和顏色'; @override String get theme => '主題'; @override String get theme_auto => '由系統定義'; - @override String get theme_light => '明亮'; - @override String get theme_dark => '黑暗'; + @override String get theme_light => '明亮主題'; + @override String get theme_dark => '黑暗主題'; @override String get amoled_mode => 'amoled mode'; @override String get amoled_mode_descr => '盡可能使用純黑色壁紙。這對 AMOLED 螢幕設備的電池略有幫助'; @override String get dynamic_colors => '動態色彩'; @@ -4437,7 +4492,7 @@ class _TranslationsGeneralClipboardZhTw implements _TranslationsGeneralClipboard @override final _TranslationsZhTw _root; // ignore: unused_field // Translations - @override String success({required Object x}) => '${x} 已複製到剪貼簿'; + @override String success({required Object x}) => '${x}已複製到剪貼簿'; @override String get error => '複製錯誤'; } @@ -4483,10 +4538,11 @@ class _TranslationsGeneralValidationsZhTw implements _TranslationsGeneralValidat @override final _TranslationsZhTw _root; // ignore: unused_field // Translations + @override String get form_error => '修正表單中指示的欄位以繼續'; @override String get required => '必填項目'; @override String get positive => '應該是積極的'; @override String min_number({required Object x}) => '應該大於${x}'; - @override String max_number({required Object x}) => '應小於 ${x}'; + @override String max_number({required Object x}) => '應小於${x}'; } // Path: financial_health.review @@ -4557,8 +4613,8 @@ class _TranslationsFinancialHealthMonthsWithoutIncomeZhTw implements _Translatio @override String get title => '存活率'; @override String get subtitle => '考慮到您的餘額,您可以在沒有收入的情況下度過多長時間'; @override String get text_zero => '按照這樣的開支,沒有收入你一個月都活不下去!'; - @override String get text_one => '按照這樣的費用,如果沒有收入,你幾乎無法生存大約一個月!'; - @override String text_other({required Object n}) => '以這樣的支出速度,如果沒有收入,您大約可以生存 ${n} 個月。'; + @override String get text_one => '按照這樣的收入,如果沒有收入,你幾乎無法生存大約一個月!'; + @override String text_other({required Object n}) => '以這樣的支出速度,如果沒有收入,您大約可以生存 ${n}個月。'; @override String get text_infinite => '以這樣的支出速度,如果沒有收入,您大約可以一生生存。'; @override String get suggestion => '請記住,建議始終將此比率保持在至少 5 個月以上。如果您發現自己沒有足夠的儲蓄緩衝,請減少不必要的開支。'; @override String get insufficient_data => '看來我們沒有足夠的開支來計算您在沒有收入的情況下可以生存多少個月。輸入幾筆交易,然後回到這裡檢查您的財務狀況'; @@ -4607,10 +4663,10 @@ class _TranslationsTransactionNextPaymentsZhTw implements _TranslationsTransacti @override String get skip_dialog_title => '跳過交易'; @override String skip_dialog_msg({required Object date}) => '此操作不可逆轉。我們會將下次移動的日期移至${date}'; @override String get accept_today => '今天接受'; - @override String accept_in_required_date({required Object date}) => '在要求的日期(${date})接受'; + @override String accept_in_required_date({required Object date}) => '在要求的日期 (${date}) 接受'; @override String get accept_dialog_title => '接受交易'; @override String get accept_dialog_msg_single => '該交易的新狀態將為空。您可以隨時重新編輯該交易的狀態'; - @override String accept_dialog_msg({required Object date}) => '此操作將建立日期為 ${date} 的新交易。您將能夠在交易頁面上查看此交易的詳細資訊'; + @override String accept_dialog_msg({required Object date}) => '此操作將建立日期為${date}的新交易。您將能夠在交易頁面上查看此交易的詳細資訊'; @override String get recurrent_rule_finished => '循環規則已完成,無需再支付!'; } @@ -4621,7 +4677,7 @@ class _TranslationsTransactionListZhTw implements _TranslationsTransactionListEn @override final _TranslationsZhTw _root; // ignore: unused_field // Translations - @override String get empty => '未發現此處顯示的交易。請點選底部的 \'+\' 按鈕新增交易'; + @override String get empty => '未發現此處顯示的交易。在應用程式中添加一些交易,也許您下次會有更好的運氣'; @override String get searcher_placeholder => '按類別、描述搜尋...'; @override String get searcher_no_results => '未找到符合搜尋條件的交易'; @override String get loading => '正在加載更多交易...'; @@ -4663,7 +4719,7 @@ class _TranslationsTransactionFormZhTw implements _TranslationsTransactionFormEn @override String get title_short => '資質'; @override String get value => '交易價值'; @override String get tap_to_see_more => '點擊查看更多詳細資訊'; - @override String get no_tags => '-- 无标签 --'; + @override String get no_tags => '-- 無標籤 --'; @override String get description => '描述'; @override String get description_info => '點擊此處輸入有關此交易的更詳細描述'; @override String exchange_to_preferred_title({required Object currency}) => '匯率為${currency}'; @@ -4677,10 +4733,10 @@ class _TranslationsTransactionReversedZhTw implements _TranslationsTransactionRe @override final _TranslationsZhTw _root; // ignore: unused_field // Translations - @override String get title => '撤销交易'; - @override String get title_short => '倒置的 tr.'; - @override String get description_for_expenses => '尽管是费用类型交易,但此交易具有正金额。这些类型的交易可用于表示先前记录的费用的返还,例如退款或支付债务。'; - @override String get description_for_incomes => '尽管是收入类型交易,但此交易的金额为负数。这些类型的交易可用于作废或更正错误记录的收入,以反映退款或退款或记录债务的支付。'; + @override String get title => '撤銷交易'; + @override String get title_short => 'Inverse tr.'; + @override String get description_for_expenses => '儘管是收入交易,但它的金額為正數。這些類型的交易可用於表示先前記錄的收入的返還,例如退款或償還債務。'; + @override String get description_for_incomes => '儘管是一項收入交易,但其金額為負數。這些類型的交易可用於作廢或更正錯誤記錄的收入,反映資金的返還或退款或記錄債務的支付。'; } // Path: transaction.status @@ -4724,12 +4780,12 @@ class _TranslationsTransactionTypesZhTw implements _TranslationsTransactionTypes other: '收入', ); @override String expense({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: '費用', - other: '花費', + one: '支出', + other: '支出', ); @override String transfer({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '轉移', - other: '轉帳', + other: '轉移', ); } @@ -4862,6 +4918,17 @@ class _TranslationsTagsFormZhTw implements _TranslationsTagsFormEn { @override String get description => '描述'; } +// Path: tags.select +class _TranslationsTagsSelectZhTw implements _TranslationsTagsSelectEn { + _TranslationsTagsSelectZhTw._(this._root); + + @override final _TranslationsZhTw _root; // ignore: unused_field + + // Translations + @override String get title => '選擇標籤'; + @override String get all => '所有標籤'; +} + // Path: categories.select class _TranslationsCategoriesSelectZhTw implements _TranslationsCategoriesSelectEn { _TranslationsCategoriesSelectZhTw._(this._root); @@ -4902,9 +4969,9 @@ class _TranslationsBudgetsDetailsZhTw implements _TranslationsBudgetsDetailsEn { @override String get title => '預算詳情'; @override String get statistics => '統計數據'; @override String get budget_value => '預算'; - @override String expend_diary_left({required Object dailyAmount, required Object remainingDays}) => '您每天可以花費 ${dailyAmount},持續剩餘 ${remainingDays} 天'; - @override String get expend_evolution => '支出演變'; - @override String get no_transactions => '看來您還沒有做出與此預算相關的任何費用'; + @override String expend_diary_left({required Object dailyAmount, required Object remainingDays}) => '您每天可以花費${dailyAmount}/天,持續${remainingDays}天'; + @override String get expend_evolution => '支出變化'; + @override String get no_transactions => '看來您還沒有做出與此預算相關的任何收入'; } // Path: backup.export @@ -4922,7 +4989,7 @@ class _TranslationsBackupExportZhTw implements _TranslationsBackupExportEn { @override String get transactions_descr => '以 CSV 格式匯出您的交易,以便您可以在其他程式或應用程式中更輕鬆地分析它們。'; @override String get description => '以不同格式下載資料'; @override String get dialog_title => '儲存/傳送文件'; - @override String success({required Object x}) => '檔案已在 ${x} 中成功儲存/下載'; + @override String success({required Object x}) => '檔案已在${x}中成功儲存/下載'; @override String get error => '下載檔案時發生錯誤。請透過 lozin.technologies@gmail.com 聯絡開發人員'; } @@ -4934,10 +5001,10 @@ class _TranslationsBackupImportZhTw implements _TranslationsBackupImportEn { // Translations @override String get title => '匯入您的資料'; - @override String get title_short => '進口'; + @override String get title_short => '匯入'; @override String get restore_backup => '恢復備份'; @override String get restore_backup_descr => '從 Monekin 匯入先前儲存的資料庫。此操作將用新資料取代任何當前應用程式資料'; - @override String get restore_backup_warn_description => '導入新資料庫時,您將丟失應用程式中當前保存的所有資料。建議在繼續之前進行備份。請勿在此處上傳任何來源不明的文件,僅上傳您之前從其下載的文件莫尼金'; + @override String get restore_backup_warn_description => '導入新資料庫時,您將丟失應用程式中當前保存的所有資料。建議在繼續之前進行備份。請勿在此處上傳任何來源不明的文件,僅上傳您之前從其下載的文件 Monekin'; @override String get restore_backup_warn_title => '覆蓋所有數據'; @override String get select_other_file => '選擇其他文件'; @override String get tap_to_select_file => '點選選擇檔案'; @@ -4958,7 +5025,7 @@ class _TranslationsBackupAboutZhTw implements _TranslationsBackupAboutEn { @override String get create_date => '創建日期'; @override String get modify_date => '上一次更改'; @override String get last_backup => '上次備份'; - @override String get size => '尺寸'; + @override String get size => '檔案大小'; } // Path: settings.security @@ -4969,12 +5036,12 @@ class _TranslationsSettingsSecurityZhTw implements _TranslationsSettingsSecurity // Translations @override String get title => '安全'; - @override String get private_mode_at_launch => '启动时的私密模式'; - @override String get private_mode_at_launch_descr => '默认以私有模式启动应用程序'; - @override String get private_mode => '私人模式'; - @override String get private_mode_descr => '隐藏所有货币值'; - @override String get private_mode_activated => '隐私模式已激活'; - @override String get private_mode_deactivated => '私人模式已禁用'; + @override String get private_mode_at_launch => '啟動時啟用的私密模式'; + @override String get private_mode_at_launch_descr => '默認以私密模式啟動應用程序'; + @override String get private_mode => '私密模式'; + @override String get private_mode_descr => '隱藏所有貨幣值'; + @override String get private_mode_activated => '私密模式已啟用'; + @override String get private_mode_deactivated => '私密模式已禁用'; } // Path: more.data @@ -4987,7 +5054,7 @@ class _TranslationsMoreDataZhTw implements _TranslationsMoreDataEn { @override String get display => '數據'; @override String get display_descr => '匯出和匯入您的數據,這樣您就不會丟失任何東西'; @override String get delete_all => '刪除我的資料'; - @override String get delete_all_header1 => '學徒就停在那裡⚠️⚠️'; + @override String get delete_all_header1 => 'Stop right there padawan ⚠️⚠️'; @override String get delete_all_message1 => '您確定要繼續嗎?您的所有資料將永久刪除且無法恢復'; @override String get delete_all_header2 => '最後一步⚠️⚠️'; @override String get delete_all_message2 => '刪除帳戶後,您將刪除所有儲存的個人資料。您的帳戶、交易、預算和類別將被刪除且無法恢復。您同意嗎?'; @@ -5014,10 +5081,10 @@ class _TranslationsMoreHelpUsZhTw implements _TranslationsMoreHelpUsEn { // Translations @override String get display => '幫助我們'; - @override String get description => '了解如何幫助莫尼金變得越來越好'; + @override String get description => '了解如何幫助 Monekin 變得越來越好'; @override String get rate_us => '評價我們'; @override String get rate_us_descr => '歡迎任何價格!'; - @override String get share => '分享莫尼金'; + @override String get share => '分享 Monekin'; @override String get share_descr => '與朋友和家人分享我們的應用程式'; @override String get share_text => 'Monekin!最好的個人理財應用程式。在這裡下載'; @override String get thanks => '謝謝你!'; @@ -5043,19 +5110,19 @@ class _TranslationsGeneralTimeRangesZhTw implements _TranslationsGeneralTimeRang @override late final _TranslationsGeneralTimeRangesTypesZhTw types = _TranslationsGeneralTimeRangesTypesZhTw._(_root); @override String each_range({required num n, required Object range}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '每個${range}', - other: '每 ${n} ${range}', + other: '每${n}${range}', ); @override String each_range_until_date({required num n, required Object range, required Object day}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: '每 ${range} 直到 ${day}', - other: '每 ${n} ${range} 直到 ${day}', + one: '每${range}直到${day}', + other: '每 ${n}${range}直到${day}', ); @override String each_range_until_times({required num n, required Object range, required Object limit}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: '每${range} ${limit}次', - other: '每 ${n} ${range} ${limit} 次', + one: '每${range}${limit}次', + other: '每${n}${range}${limit}次', ); @override String each_range_until_once({required num n, required Object range}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '每${range}一次', - other: '每 ${n} ${range} 一次', + other: '每${n}${range}一次', ); @override String month({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '月', @@ -5082,7 +5149,7 @@ class _TranslationsGeneralTimePeriodicityZhTw implements _TranslationsGeneralTim @override final _TranslationsZhTw _root; // ignore: unused_field // Translations - @override String get display => '復發'; + @override String get display => '週期性'; @override String get no_repeat => '不再重複'; @override String repeat({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '重複', @@ -5133,7 +5200,7 @@ class _TranslationsFinancialHealthReviewDescrZhTw implements _TranslationsFinanc @override final _TranslationsZhTw _root; // ignore: unused_field // Translations - @override String get insufficient_data => '看起來我們沒有足夠的費用來計算您的財務健康狀況。添加這段時間的一些費用/收入,以便我們能夠幫助您!'; + @override String get insufficient_data => '看起來我們沒有足夠的收入來計算您的財務健康狀況。添加這段時間的一些收入/收入,以便我們能夠幫助您!'; @override String get very_good => '恭喜!您的財務狀況非常好。我們希望您繼續保持良好的勢頭,並繼續與 Monekin 一起學習'; @override String get good => '太棒了!您的財務狀況良好。請訪問分析選項卡,了解如何節省更多!'; @override String get normal => '您的財務健康狀況或多或少處於這一時期其他人口的平均水平'; @@ -5163,7 +5230,7 @@ class _TranslationsTransactionListBulkEditZhTw implements _TranslationsTransacti // Translations @override String get dates => '編輯日期'; @override String get categories => '編輯類別'; - @override String get status => '编辑状态'; + @override String get status => '編輯狀態'; } // Path: transaction.form.validators @@ -5187,8 +5254,8 @@ class _TranslationsTransferFormValueInDestinyZhTw implements _TranslationsTransf @override final _TranslationsZhTw _root; // ignore: unused_field // Translations - @override String get title => '目的地转账金额'; - @override String amount_short({required Object amount}) => '${amount} 至目标账户'; + @override String get title => '目的地轉帳金额'; + @override String amount_short({required Object amount}) => '${amount}至目標帳户'; } // Path: backup.import.manual_import @@ -5214,13 +5281,13 @@ class _TranslationsBackupImportManualImportZhTw implements _TranslationsBackupIm ]; @override List get steps_descr => [ '從您的裝置中選擇一個 .csv 檔案。確保它的第一行描述了每列的名稱。', - '選擇指定每筆交易價值的欄位。使用負值表示支出,使用正值表示收入。使用點作為小數點分隔符號。', - '選擇指定每筆交易所屬帳戶的列。您也可以選擇預設帳戶,以防我們找不到您想要的帳戶。如果未指定預設帳戶,我們將建立一個同名帳戶。', - '指定交易類別名稱所在的欄位。您必須指定一個預設類別,以便我們將此類別指派給交易,以防找不到該類別。', - '選擇指定每筆交易日期的欄位。如果未指定,交易將使用當前日期建立。', - 'Specifies the columns for other optional transaction attributes', + '選擇每筆交易價值的欄位。使用負值表示支出,使用正值表示收入。使用點作為小數點分隔符號。', + '選擇每筆交易所屬帳戶的列。您也可以選擇預設帳戶,以防我們找不到您想要的帳戶。如果未指定預設帳戶,我們將建立一個同名帳戶。', + '選擇交易類別名稱所在的欄位。您必須指定一個預設類別,以便我們將此類別指派給交易,以防找不到該類別。', + '選擇每筆交易日期的欄位。如果未指定,交易將使用當前日期建立。', + '選擇其他交易屬性的資料列', ]; - @override String success({required Object x}) => '已成功導入 ${x} 筆交易'; + @override String success({required Object x}) => '已成功導入${x}筆交易'; } // Path: more.about_us.legal @@ -5245,7 +5312,7 @@ class _TranslationsMoreAboutUsProjectZhTw implements _TranslationsMoreAboutUsPro // Translations @override String get display => '專案'; @override String get contributors => '合作者'; - @override String get contributors_descr => '所有讓莫尼金成長的開發者'; + @override String get contributors_descr => '所有讓 Monekin 成長的開發者'; @override String get contact => '聯絡我們'; } @@ -5258,7 +5325,7 @@ class _TranslationsGeneralTimeRangesTypesZhTw implements _TranslationsGeneralTim // Translations @override String get cycle => '循環'; @override String get last_days => '最後一天'; - @override String last_days_form({required Object x}) => '前 ${x} 天'; + @override String last_days_form({required Object x}) => '前${x}天'; @override String get all => '總是'; @override String get date_range => '自訂範圍'; } @@ -5290,6 +5357,7 @@ extension on Translations { case 'general.today': return 'Today'; case 'general.yesterday': return 'Yesterday'; case 'general.filters': return 'Filters'; + case 'general.see_more': return 'See more'; case 'general.select_all': return 'Select all'; case 'general.deselect_all': return 'Deselect all'; case 'general.empty_warn': return 'Ops! This is very empty'; @@ -5377,6 +5445,7 @@ extension on Translations { case 'general.transaction_order.category': return 'By category'; case 'general.transaction_order.quantity': return 'By quantity'; case 'general.transaction_order.date': return 'By date'; + case 'general.validations.form_error': return 'Fix the indicated fields to continue'; case 'general.validations.required': return 'Required field'; case 'general.validations.positive': return 'Should be positive'; case 'general.validations.min_number': return ({required Object x}) => 'Should be greater than ${x}'; @@ -5485,10 +5554,14 @@ extension on Translations { case 'stats.balance': return 'Balance'; case 'stats.final_balance': return 'Final balance'; case 'stats.balance_by_account': return 'Balance by accounts'; + case 'stats.balance_by_account_subtitle': return 'Where do I have most of my money?'; case 'stats.balance_by_currency': return 'Balance by currency'; - case 'stats.cash_flow': return 'Cash flow'; - case 'stats.balance_evolution': return 'Balance evolution'; + case 'stats.balance_by_currency_subtitle': return 'How much money do I have in foreign currency?'; + case 'stats.balance_evolution': return 'Balance trend'; + case 'stats.balance_evolution_subtitle': return 'Do I have more money than before?'; case 'stats.compared_to_previous_period': return 'Compared to the previous period'; + case 'stats.cash_flow': return 'Cash flow'; + case 'stats.cash_flow_subtitle': return 'Am I spending less than I earn?'; case 'stats.by_periods': return 'By periods'; case 'stats.by_categories': return 'By categories'; case 'stats.by_tags': return 'By tags'; @@ -5544,7 +5617,7 @@ extension on Translations { case 'transaction.next_payments.accept_dialog_msg_single': return 'The new status of the transaction will be null. You can re-edit the status of this transaction whenever you want'; case 'transaction.next_payments.accept_dialog_msg': return ({required Object date}) => 'This action will create a new transaction with date ${date}. You will be able to check the details of this transaction on the transaction page'; case 'transaction.next_payments.recurrent_rule_finished': return 'The recurring rule has been completed, there are no more payments to make!'; - case 'transaction.list.empty': return 'No transactions found to display here. Add a transaction by clicking the \'+\' button at the bottom'; + case 'transaction.list.empty': return 'No transactions found to display here. Add a few transactions in the app and maybe you\'ll have better luck next time.'; case 'transaction.list.searcher_placeholder': return 'Search by category, description...'; case 'transaction.list.searcher_no_results': return 'No transactions found matching the search criteria'; case 'transaction.list.loading': return 'Loading more transactions...'; @@ -5704,9 +5777,10 @@ extension on Translations { ); case 'tags.form.name': return 'Tag name'; case 'tags.form.description': return 'Description'; + case 'tags.select.title': return 'Select tags'; + case 'tags.select.all': return 'All the tags'; case 'tags.empty_list': return 'You haven\'t created any tags yet. Tags and categories are a great way to categorize your movements'; case 'tags.without_tags': return 'Without tags'; - case 'tags.select': return 'Select tags'; case 'tags.add': return 'Add tag'; case 'tags.create': return 'Create label'; case 'tags.create_success': return 'Label created successfully'; @@ -5910,6 +5984,7 @@ extension on _TranslationsEs { case 'general.today': return 'Hoy'; case 'general.yesterday': return 'Ayer'; case 'general.filters': return 'Filtros'; + case 'general.see_more': return 'Ver más'; case 'general.select_all': return 'Seleccionar todo'; case 'general.deselect_all': return 'Deseleccionar todo'; case 'general.empty_warn': return 'Ops! Esto esta muy vacio'; @@ -5998,6 +6073,7 @@ extension on _TranslationsEs { case 'general.transaction_order.category': return 'Por categoría'; case 'general.transaction_order.quantity': return 'Por cantidad'; case 'general.transaction_order.date': return 'Por fecha'; + case 'general.validations.form_error': return 'Corrije los campos indicados en el formulario para continuar'; case 'general.validations.required': return 'Campo obligatorio'; case 'general.validations.positive': return 'Debe ser positivo'; case 'general.validations.min_number': return ({required Object x}) => 'Debe ser mayor que ${x}'; @@ -6020,7 +6096,7 @@ extension on _TranslationsEs { case 'intro.last_slide_title': return 'Todo listo!'; case 'intro.last_slide_descr': return 'Con Monekin, podrás al fin lograr la independencia financiaria que tanto deseas. Podrás ver gráficas, presupuestos, consejos, estadisticas y mucho más sobre tu dinero.'; case 'intro.last_slide_descr2': return 'Esperemos que disfrutes de tu experiencia! No dudes en contactar con nosotros en caso de dudas, sugerencias...'; - case 'home.title': return 'Dashboard'; + case 'home.title': return 'Inicio'; case 'home.filter_transactions': return 'Filtrar transacciones'; case 'home.hello_day': return 'Buenos días,'; case 'home.hello_night': return 'Buenas noches,'; @@ -6106,10 +6182,14 @@ extension on _TranslationsEs { case 'stats.balance': return 'Saldo'; case 'stats.final_balance': return 'Saldo final'; case 'stats.balance_by_account': return 'Saldo por cuentas'; + case 'stats.balance_by_account_subtitle': return '¿Donde tengo la mayor parte de mi dinero?'; case 'stats.balance_by_currency': return 'Saldo por divisas'; + case 'stats.balance_by_currency_subtitle': return '¿Cuanto dinero tengo en moneda extranjera?'; case 'stats.balance_evolution': return 'Tendencia de saldo'; + case 'stats.balance_evolution_subtitle': return '¿Tengo más dinero que antes?'; case 'stats.compared_to_previous_period': return 'Frente al periodo anterior'; case 'stats.cash_flow': return 'Flujo de caja'; + case 'stats.cash_flow_subtitle': return '¿Estoy gastando menos de lo que gano?'; case 'stats.by_periods': return 'Por periodos'; case 'stats.by_categories': return 'Por categorías'; case 'stats.by_tags': return 'Por etiquetas'; @@ -6165,7 +6245,7 @@ extension on _TranslationsEs { case 'transaction.next_payments.accept_dialog_msg_single': return 'El estado de la transacción pasará a ser nulo. Puedes volver a editar el estado de esta transacción cuando lo desees'; case 'transaction.next_payments.accept_dialog_msg': return ({required Object date}) => 'Esta acción creará una transacción nueva con fecha ${date}. Podrás consultar los detalles de esta transacción en la página de transacciones'; case 'transaction.next_payments.recurrent_rule_finished': return 'La regla recurrente se ha completado, ya no hay mas pagos a realizar!'; - case 'transaction.list.empty': return 'No se han encontrado transacciones que mostrar aquí. Añade una transacción pulsando el botón \'+\' de la parte inferior'; + case 'transaction.list.empty': return 'No se han encontrado transacciones que mostrar aquí. Añade unas cuantas transacciones en la app y quizas tengas más suerte la proxima vez'; case 'transaction.list.searcher_placeholder': return 'Busca por categoría, descripción...'; case 'transaction.list.searcher_no_results': return 'No se han encontrado transacciones que coincidan con los criterios de busqueda'; case 'transaction.list.loading': return 'Cargando más transacciones...'; @@ -6325,9 +6405,10 @@ extension on _TranslationsEs { ); case 'tags.form.name': return 'Nombre de la etiqueta'; case 'tags.form.description': return 'Descripción'; + case 'tags.select.title': return 'Selecciona etiquetas'; + case 'tags.select.all': return 'Todas las etiquetas'; case 'tags.empty_list': return 'No has creado ninguna etiqueta aun. Las etiquetas y las categorías son una gran forma de categorizar tus movimientos'; case 'tags.without_tags': return 'Sin etiquetas'; - case 'tags.select': return 'Selecionar etiquetas'; case 'tags.create': return 'Crear etiqueta'; case 'tags.add': return 'Añadir etiqueta'; case 'tags.create_success': return 'Etiqueta creada correctamente'; @@ -6531,6 +6612,7 @@ extension on _TranslationsUk { case 'general.today': return 'Сьогодні'; case 'general.yesterday': return 'Вчора'; case 'general.filters': return 'Фільтри'; + case 'general.see_more': return 'Побачити більше'; case 'general.select_all': return 'Вибрати всі'; case 'general.deselect_all': return 'Скасувати вибір усіх'; case 'general.empty_warn': return 'Ой! Тут порожньо'; @@ -6618,6 +6700,7 @@ extension on _TranslationsUk { case 'general.transaction_order.category': return 'За категорією'; case 'general.transaction_order.quantity': return 'За кількістю'; case 'general.transaction_order.date': return 'За датою'; + case 'general.validations.form_error': return 'Виправте поля, зазначені у формі, щоб продовжити'; case 'general.validations.required': return 'Обов\'язкове поле'; case 'general.validations.positive': return 'Повинно бути позитивним'; case 'general.validations.min_number': return ({required Object x}) => 'Повинно бути більшим, ніж ${x}'; @@ -6726,10 +6809,14 @@ extension on _TranslationsUk { case 'stats.balance': return 'Баланс'; case 'stats.final_balance': return 'Кінцевий баланс'; case 'stats.balance_by_account': return 'Баланс за рахунками'; - case 'stats.balance_by_currency': return 'Баланс за валютами'; - case 'stats.cash_flow': return 'Грошовий потік'; - case 'stats.balance_evolution': return 'Еволюція балансу'; + case 'stats.balance_by_account_subtitle': return 'Де я маю більшість грошей?'; + case 'stats.balance_by_currency': return 'Баланс за валютою'; + case 'stats.balance_by_currency_subtitle': return 'Скільки я маю грошей в іноземній валюті?'; + case 'stats.balance_evolution': return 'Тенденція балансу'; + case 'stats.balance_evolution_subtitle': return 'У мене більше грошей, ніж раніше?'; case 'stats.compared_to_previous_period': return 'Порівняно з попереднім періодом'; + case 'stats.cash_flow': return 'Грошовий потік'; + case 'stats.cash_flow_subtitle': return 'Я витрачаю менше, ніж заробляю?'; case 'stats.by_periods': return 'За періодами'; case 'stats.by_categories': return 'За категоріями'; case 'stats.by_tags': return 'За тегами'; @@ -6785,7 +6872,7 @@ extension on _TranslationsUk { case 'transaction.next_payments.accept_dialog_msg_single': return 'Новий статус транзакції буде нульовим. Ви можете знову редагувати статус цієї транзакції в будь-який момент'; case 'transaction.next_payments.accept_dialog_msg': return ({required Object date}) => 'Ця дія створить нову транзакцію з датою ${date}. Ви зможете переглянути деталі цієї транзакції на сторінці транзакцій'; case 'transaction.next_payments.recurrent_rule_finished': return 'Правило періодичності було завершено, більше немає платежів!'; - case 'transaction.list.empty': return 'Тут не знайдено жодних транзакцій для відображення. Додайте транзакцію, натиснувши кнопку \'+\' внизу'; + case 'transaction.list.empty': return 'Тут не знайдено жодних транзакцій для відображення. Додайте кілька транзакцій у додаток, і, можливо, наступного разу вам пощастить більше'; case 'transaction.list.searcher_placeholder': return 'Шукати за категорією, описом...'; case 'transaction.list.searcher_no_results': return 'Не знайдено транзакцій, що відповідають критеріям пошуку'; case 'transaction.list.loading': return 'Завантаження додаткових транзакцій...'; @@ -6945,9 +7032,10 @@ extension on _TranslationsUk { ); case 'tags.form.name': return 'Назва тегу'; case 'tags.form.description': return 'Опис'; + case 'tags.select.title': return 'Вибрати теги'; + case 'tags.select.all': return 'Усі теги'; case 'tags.empty_list': return 'Ви ще не створили жодних тегів. Теги та категорії - це відмінний спосіб категоризувати ваші рухи'; case 'tags.without_tags': return 'Без тегів'; - case 'tags.select': return 'Вибрати теги'; case 'tags.add': return 'Додати тег'; case 'tags.create': return 'Створити мітку'; case 'tags.create_success': return 'Мітка успішно створена'; @@ -7137,7 +7225,7 @@ extension on _TranslationsZhTw { case 'general.confirm': return '確認'; case 'general.continue_text': return '繼續'; case 'general.quick_actions': return '快速行動'; - case 'general.save': return '節省'; + case 'general.save': return '保存'; case 'general.save_changes': return '儲存變更'; case 'general.close_and_save': return '儲存並關閉'; case 'general.add': return '添加'; @@ -7151,6 +7239,7 @@ extension on _TranslationsZhTw { case 'general.today': return '今天'; case 'general.yesterday': return '昨天'; case 'general.filters': return '過濾器'; + case 'general.see_more': return '查看更多'; case 'general.select_all': return '全選'; case 'general.deselect_all': return '取消全選'; case 'general.empty_warn': return '哦!這裡非常空'; @@ -7158,7 +7247,7 @@ extension on _TranslationsZhTw { case 'general.show_more_fields': return '顯示更多欄位'; case 'general.show_less_fields': return '顯示較少的欄位'; case 'general.tap_to_search': return '點擊即可搜尋'; - case 'general.clipboard.success': return ({required Object x}) => '${x} 已複製到剪貼簿'; + case 'general.clipboard.success': return ({required Object x}) => '${x}已複製到剪貼簿'; case 'general.clipboard.error': return '複製錯誤'; case 'general.time.start_date': return '開始日期'; case 'general.time.end_date': return '結束日期'; @@ -7175,24 +7264,24 @@ extension on _TranslationsZhTw { case 'general.time.ranges.forever': return '永遠'; case 'general.time.ranges.types.cycle': return '循環'; case 'general.time.ranges.types.last_days': return '最後一天'; - case 'general.time.ranges.types.last_days_form': return ({required Object x}) => '前 ${x} 天'; + case 'general.time.ranges.types.last_days_form': return ({required Object x}) => '前${x}天'; case 'general.time.ranges.types.all': return '總是'; case 'general.time.ranges.types.date_range': return '自訂範圍'; case 'general.time.ranges.each_range': return ({required num n, required Object range}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '每個${range}', - other: '每 ${n} ${range}', + other: '每${n}${range}', ); case 'general.time.ranges.each_range_until_date': return ({required num n, required Object range, required Object day}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: '每 ${range} 直到 ${day}', - other: '每 ${n} ${range} 直到 ${day}', + one: '每${range}直到${day}', + other: '每 ${n}${range}直到${day}', ); case 'general.time.ranges.each_range_until_times': return ({required num n, required Object range, required Object limit}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: '每${range} ${limit}次', - other: '每 ${n} ${range} ${limit} 次', + one: '每${range}${limit}次', + other: '每${n}${range}${limit}次', ); case 'general.time.ranges.each_range_until_once': return ({required num n, required Object range}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '每${range}一次', - other: '每 ${n} ${range} 一次', + other: '每${n}${range}一次', ); case 'general.time.ranges.month': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '月', @@ -7210,7 +7299,7 @@ extension on _TranslationsZhTw { one: '星期', other: '幾週', ); - case 'general.time.periodicity.display': return '復發'; + case 'general.time.periodicity.display': return '週期性'; case 'general.time.periodicity.no_repeat': return '不再重複'; case 'general.time.periodicity.repeat': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '重複', @@ -7238,10 +7327,11 @@ extension on _TranslationsZhTw { case 'general.transaction_order.category': return '按類別'; case 'general.transaction_order.quantity': return '按數量'; case 'general.transaction_order.date': return '按日期'; + case 'general.validations.form_error': return '修正表單中指示的欄位以繼續'; case 'general.validations.required': return '必填項目'; case 'general.validations.positive': return '應該是積極的'; case 'general.validations.min_number': return ({required Object x}) => '應該大於${x}'; - case 'general.validations.max_number': return ({required Object x}) => '應小於 ${x}'; + case 'general.validations.max_number': return ({required Object x}) => '應小於${x}'; case 'intro.start': return '開始'; case 'intro.skip': return '跳過'; case 'intro.next': return '下一個'; @@ -7321,7 +7411,7 @@ extension on _TranslationsZhTw { return '數據不足'; } }; - case 'financial_health.review.descr.insufficient_data': return '看起來我們沒有足夠的費用來計算您的財務健康狀況。添加這段時間的一些費用/收入,以便我們能夠幫助您!'; + case 'financial_health.review.descr.insufficient_data': return '看起來我們沒有足夠的收入來計算您的財務健康狀況。添加這段時間的一些收入/收入,以便我們能夠幫助您!'; case 'financial_health.review.descr.very_good': return '恭喜!您的財務狀況非常好。我們希望您繼續保持良好的勢頭,並繼續與 Monekin 一起學習'; case 'financial_health.review.descr.good': return '太棒了!您的財務狀況良好。請訪問分析選項卡,了解如何節省更多!'; case 'financial_health.review.descr.normal': return '您的財務健康狀況或多或少處於這一時期其他人口的平均水平'; @@ -7330,8 +7420,8 @@ extension on _TranslationsZhTw { case 'financial_health.months_without_income.title': return '存活率'; case 'financial_health.months_without_income.subtitle': return '考慮到您的餘額,您可以在沒有收入的情況下度過多長時間'; case 'financial_health.months_without_income.text_zero': return '按照這樣的開支,沒有收入你一個月都活不下去!'; - case 'financial_health.months_without_income.text_one': return '按照這樣的費用,如果沒有收入,你幾乎無法生存大約一個月!'; - case 'financial_health.months_without_income.text_other': return ({required Object n}) => '以這樣的支出速度,如果沒有收入,您大約可以生存 ${n} 個月。'; + case 'financial_health.months_without_income.text_one': return '按照這樣的收入,如果沒有收入,你幾乎無法生存大約一個月!'; + case 'financial_health.months_without_income.text_other': return ({required Object n}) => '以這樣的支出速度,如果沒有收入,您大約可以生存 ${n}個月。'; case 'financial_health.months_without_income.text_infinite': return '以這樣的支出速度,如果沒有收入,您大約可以一生生存。'; case 'financial_health.months_without_income.suggestion': return '請記住,建議始終將此比率保持在至少 5 個月以上。如果您發現自己沒有足夠的儲蓄緩衝,請減少不必要的開支。'; case 'financial_health.months_without_income.insufficient_data': return '看來我們沒有足夠的開支來計算您在沒有收入的情況下可以生存多少個月。輸入幾筆交易,然後回到這裡檢查您的財務狀況'; @@ -7346,10 +7436,14 @@ extension on _TranslationsZhTw { case 'stats.balance': return '平衡'; case 'stats.final_balance': return '最終餘額'; case 'stats.balance_by_account': return '帳戶餘額'; - case 'stats.balance_by_currency': return '按貨幣劃分的餘額'; - case 'stats.cash_flow': return '現金週轉'; - case 'stats.balance_evolution': return 'Balance evolution'; - case 'stats.compared_to_previous_period': return '與前期相比'; + case 'stats.balance_by_account_subtitle': return '我的大部分钱都在哪里?'; + case 'stats.balance_by_currency': return '按货币余额'; + case 'stats.balance_by_currency_subtitle': return '我有多少钱的外币?'; + case 'stats.balance_evolution': return '平衡趋势'; + case 'stats.balance_evolution_subtitle': return '我的钱比以前多了吗?'; + case 'stats.compared_to_previous_period': return '与上一时期相比'; + case 'stats.cash_flow': return '现金流'; + case 'stats.cash_flow_subtitle': return '我的支出是否少于我的收入?'; case 'stats.by_periods': return '按時期'; case 'stats.by_categories': return '按類別'; case 'stats.by_tags': return '按標籤'; @@ -7382,7 +7476,7 @@ extension on _TranslationsZhTw { case 'transaction.edit': return '編輯交易'; case 'transaction.edit_success': return '交易編輯成功'; case 'transaction.edit_multiple': return '編輯交易'; - case 'transaction.edit_multiple_success': return ({required Object x}) => '${x} 筆交易已成功編輯'; + case 'transaction.edit_multiple_success': return ({required Object x}) => '${x}筆交易已成功編輯'; case 'transaction.duplicate': return '克隆交易'; case 'transaction.duplicate_short': return '複製'; case 'transaction.duplicate_warning_message': return '將在同一日期創建與此相同的交易,您想繼續嗎?'; @@ -7391,8 +7485,8 @@ extension on _TranslationsZhTw { case 'transaction.delete_warning_message': return '此操作不可逆轉。您的帳戶當前餘額和所有統計資料都將重新計算'; case 'transaction.delete_success': return '交易已正確刪除'; case 'transaction.delete_multiple': return '刪除交易'; - case 'transaction.delete_multiple_warning_message': return ({required Object x}) => '此操作不可逆轉,將刪除 ${x} 筆交易。您帳戶的當前餘額和所有統計資料都將重新計算'; - case 'transaction.delete_multiple_success': return ({required Object x}) => '正確刪除了 ${x} 筆交易'; + case 'transaction.delete_multiple_warning_message': return ({required Object x}) => '此操作不可逆轉,將刪除${x}筆交易。您帳戶的當前餘額和所有統計資料都將重新計算'; + case 'transaction.delete_multiple_success': return ({required Object x}) => '正確刪除了${x}筆交易'; case 'transaction.details': return '動作詳情'; case 'transaction.next_payments.accept': return '接受'; case 'transaction.next_payments.skip': return '跳過'; @@ -7400,12 +7494,12 @@ extension on _TranslationsZhTw { case 'transaction.next_payments.skip_dialog_title': return '跳過交易'; case 'transaction.next_payments.skip_dialog_msg': return ({required Object date}) => '此操作不可逆轉。我們會將下次移動的日期移至${date}'; case 'transaction.next_payments.accept_today': return '今天接受'; - case 'transaction.next_payments.accept_in_required_date': return ({required Object date}) => '在要求的日期(${date})接受'; + case 'transaction.next_payments.accept_in_required_date': return ({required Object date}) => '在要求的日期 (${date}) 接受'; case 'transaction.next_payments.accept_dialog_title': return '接受交易'; case 'transaction.next_payments.accept_dialog_msg_single': return '該交易的新狀態將為空。您可以隨時重新編輯該交易的狀態'; - case 'transaction.next_payments.accept_dialog_msg': return ({required Object date}) => '此操作將建立日期為 ${date} 的新交易。您將能夠在交易頁面上查看此交易的詳細資訊'; + case 'transaction.next_payments.accept_dialog_msg': return ({required Object date}) => '此操作將建立日期為${date}的新交易。您將能夠在交易頁面上查看此交易的詳細資訊'; case 'transaction.next_payments.recurrent_rule_finished': return '循環規則已完成,無需再支付!'; - case 'transaction.list.empty': return '未發現此處顯示的交易。請點選底部的 \'+\' 按鈕新增交易'; + case 'transaction.list.empty': return '未發現此處顯示的交易。在應用程式中添加一些交易,也許您下次會有更好的運氣'; case 'transaction.list.searcher_placeholder': return '按類別、描述搜尋...'; case 'transaction.list.searcher_no_results': return '未找到符合搜尋條件的交易'; case 'transaction.list.loading': return '正在加載更多交易...'; @@ -7419,7 +7513,7 @@ extension on _TranslationsZhTw { ); case 'transaction.list.bulk_edit.dates': return '編輯日期'; case 'transaction.list.bulk_edit.categories': return '編輯類別'; - case 'transaction.list.bulk_edit.status': return '编辑状态'; + case 'transaction.list.bulk_edit.status': return '編輯狀態'; case 'transaction.filters.from_value': return '從金額'; case 'transaction.filters.to_value': return '最多金額'; case 'transaction.filters.from_value_def': return ({required Object x}) => '來自 ${x}'; @@ -7435,15 +7529,15 @@ extension on _TranslationsZhTw { case 'transaction.form.title_short': return '資質'; case 'transaction.form.value': return '交易價值'; case 'transaction.form.tap_to_see_more': return '點擊查看更多詳細資訊'; - case 'transaction.form.no_tags': return '-- 无标签 --'; + case 'transaction.form.no_tags': return '-- 無標籤 --'; case 'transaction.form.description': return '描述'; case 'transaction.form.description_info': return '點擊此處輸入有關此交易的更詳細描述'; case 'transaction.form.exchange_to_preferred_title': return ({required Object currency}) => '匯率為${currency}'; case 'transaction.form.exchange_to_preferred_in_date': return '交易日'; - case 'transaction.reversed.title': return '撤销交易'; - case 'transaction.reversed.title_short': return '倒置的 tr.'; - case 'transaction.reversed.description_for_expenses': return '尽管是费用类型交易,但此交易具有正金额。这些类型的交易可用于表示先前记录的费用的返还,例如退款或支付债务。'; - case 'transaction.reversed.description_for_incomes': return '尽管是收入类型交易,但此交易的金额为负数。这些类型的交易可用于作废或更正错误记录的收入,以反映退款或退款或记录债务的支付。'; + case 'transaction.reversed.title': return '撤銷交易'; + case 'transaction.reversed.title_short': return 'Inverse tr.'; + case 'transaction.reversed.description_for_expenses': return '儘管是收入交易,但它的金額為正數。這些類型的交易可用於表示先前記錄的收入的返還,例如退款或償還債務。'; + case 'transaction.reversed.description_for_incomes': return '儘管是一項收入交易,但其金額為負數。這些類型的交易可用於作廢或更正錯誤記錄的收入,反映資金的返還或退款或記錄債務的支付。'; case 'transaction.status.display': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '地位', other: '狀態', @@ -7469,27 +7563,27 @@ extension on _TranslationsZhTw { other: '收入', ); case 'transaction.types.expense': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: '費用', - other: '花費', + one: '支出', + other: '支出', ); case 'transaction.types.transfer': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, one: '轉移', - other: '轉帳', + other: '轉移', ); case 'transfer.display': return '轉移'; - case 'transfer.transfers': return '轉帳'; - case 'transfer.transfer_to': return ({required Object account}) => '轉帳至${account}'; - case 'transfer.create': return '新轉移'; + case 'transfer.transfers': return '轉移'; + case 'transfer.transfer_to': return ({required Object account}) => '轉移至${account}'; + case 'transfer.create': return '轉移'; case 'transfer.need_two_accounts_warning_header': return 'Ops!'; case 'transfer.need_two_accounts_warning_message': return '至少需要兩個帳戶才能執行此操作。如果您需要調整或編輯該帳戶的當前餘額,請點擊編輯按鈕'; case 'transfer.form.from': return '原始帳戶'; case 'transfer.form.to': return '目的地帳戶'; - case 'transfer.form.value_in_destiny.title': return '目的地转账金额'; - case 'transfer.form.value_in_destiny.amount_short': return ({required Object amount}) => '${amount} 至目标账户'; + case 'transfer.form.value_in_destiny.title': return '目的地轉帳金额'; + case 'transfer.form.value_in_destiny.amount_short': return ({required Object amount}) => '${amount}至目標帳户'; case 'recurrent_transactions.title': return '經常性交易'; case 'recurrent_transactions.title_short': return '記錄交易'; case 'recurrent_transactions.empty': return '您似乎沒有任何經常性交易。創建每月、每年或每週的經常性交易,它將顯示在此處'; - case 'recurrent_transactions.total_expense_title': return '每個期間的總費用'; + case 'recurrent_transactions.total_expense_title': return '每個期間的總收入'; case 'recurrent_transactions.total_expense_descr': return '*不考慮每次重複的開始和結束日期'; case 'recurrent_transactions.details.title': return '經常性交易'; case 'recurrent_transactions.details.descr': return '此交易的下一步動作如下所示。您可以接受第一步動作或跳過此動作'; @@ -7504,8 +7598,8 @@ extension on _TranslationsZhTw { case 'account.reopen_descr': return '您確定要重新開啟此帳戶嗎?'; case 'account.balance': return '帳戶餘額'; case 'account.n_transactions': return '交易數量'; - case 'account.add_money': return '加錢'; - case 'account.withdraw_money': return '取錢'; + case 'account.add_money': return '增加金額'; + case 'account.withdraw_money': return '取出金額'; case 'account.no_accounts': return '未發現此處顯示的交易。請點選底部的 \'+\' 按鈕新增交易'; case 'account.types.title': return '帳戶類型'; case 'account.types.warning': return '帳戶類型一旦選擇,以後將無法更改'; @@ -7541,7 +7635,7 @@ extension on _TranslationsZhTw { case 'account.select.multiple': return '選擇帳戶'; case 'currencies.currency_converter': return '貨幣換算'; case 'currencies.currency': return '貨幣'; - case 'currencies.currency_manager': return '貨幣經理'; + case 'currencies.currency_manager': return '貨幣管理'; case 'currencies.currency_manager_descr': return '配置您的貨幣及其與其他貨幣的匯率'; case 'currencies.preferred_currency': return '首選/基礎貨幣'; case 'currencies.change_preferred_currency_title': return '更改首選貨幣'; @@ -7560,14 +7654,15 @@ extension on _TranslationsZhTw { case 'currencies.select_a_currency': return '選擇貨幣'; case 'currencies.search': return '按名稱或貨幣代碼搜尋'; case 'tags.display': return ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('zh'))(n, - one: 'one', - other: 'other', + one: '標籤', + other: '標籤', ); case 'tags.form.name': return '標籤名'; case 'tags.form.description': return '描述'; + case 'tags.select.title': return '選擇標籤'; + case 'tags.select.all': return '所有標籤'; case 'tags.empty_list': return '您還沒有創建任何標籤。標籤和類別是對您的動作進行分類的好方法'; case 'tags.without_tags': return '沒有標籤'; - case 'tags.select': return '選擇標籤'; case 'tags.add': return '添加標籤'; case 'tags.create': return '建立標籤'; case 'tags.create_success': return '標籤創建成功'; @@ -7581,7 +7676,7 @@ extension on _TranslationsZhTw { case 'categories.create': return '創建類別'; case 'categories.create_success': return '類別創建正確'; case 'categories.new_category': return '新類別'; - case 'categories.already_exists': return '該類別的名稱已存在。也許您想編輯它'; + case 'categories.already_exists': return '該類別的名稱已存在。也許您可以編輯'; case 'categories.edit': return '編輯類別'; case 'categories.edit_success': return '類別編輯正確'; case 'categories.name': return '分類名稱'; @@ -7595,7 +7690,7 @@ extension on _TranslationsZhTw { case 'categories.make_child_warning2': return ({required Object x, required Object destiny}) => '他們的交易(${x})將被移至${destiny}類別中創建的新子類別。'; case 'categories.make_child_success': return '子類別創建成功'; case 'categories.merge': return '與另一個類別合併'; - case 'categories.merge_warning1': return ({required Object from, required Object x, required Object destiny}) => '與類別 ${from} 相關的所有交易 (${x}) 將移至類別 ${destiny}'; + case 'categories.merge_warning1': return ({required Object from, required Object x, required Object destiny}) => '與類別${from}相關的所有交易 (${x}) 將移至類別${destiny}'; case 'categories.merge_warning2': return ({required Object from}) => '類別${from}將被不可逆轉地刪除。 '; case 'categories.merge_success': return '類別合併成功'; case 'categories.delete_success': return '類別已正確刪除'; @@ -7607,16 +7702,16 @@ extension on _TranslationsZhTw { case 'categories.select.without_subcategory': return '沒有子類別'; case 'categories.select.all': return '所有類別'; case 'categories.select.all_short': return '全部'; - case 'budgets.title': return 'title'; + case 'budgets.title': return '預算'; case 'budgets.repeated': return '再次發生的'; case 'budgets.one_time': return '一次'; - case 'budgets.annual': return 'annual'; + case 'budgets.annual': return '年度'; case 'budgets.week': return '每週'; case 'budgets.month': return '每月'; case 'budgets.actives': return '活躍'; case 'budgets.pending': return '等待開始'; case 'budgets.finish': return '完成的'; - case 'budgets.from_budgeted': return '的左邊 '; + case 'budgets.from_budgeted': return '從預算'; case 'budgets.days_left': return '還剩幾天'; case 'budgets.days_to_start': return '開始的日子'; case 'budgets.since_expiration': return '自到期日起的天數'; @@ -7632,9 +7727,9 @@ extension on _TranslationsZhTw { case 'budgets.details.title': return '預算詳情'; case 'budgets.details.statistics': return '統計數據'; case 'budgets.details.budget_value': return '預算'; - case 'budgets.details.expend_diary_left': return ({required Object dailyAmount, required Object remainingDays}) => '您每天可以花費 ${dailyAmount},持續剩餘 ${remainingDays} 天'; - case 'budgets.details.expend_evolution': return '支出演變'; - case 'budgets.details.no_transactions': return '看來您還沒有做出與此預算相關的任何費用'; + case 'budgets.details.expend_diary_left': return ({required Object dailyAmount, required Object remainingDays}) => '您每天可以花費${dailyAmount}/天,持續${remainingDays}天'; + case 'budgets.details.expend_evolution': return '支出變化'; + case 'budgets.details.no_transactions': return '看來您還沒有做出與此預算相關的任何收入'; case 'backup.export.title': return '匯出您的資料'; case 'backup.export.title_short': return '匯出'; case 'backup.export.all': return '完整備份'; @@ -7643,13 +7738,13 @@ extension on _TranslationsZhTw { case 'backup.export.transactions_descr': return '以 CSV 格式匯出您的交易,以便您可以在其他程式或應用程式中更輕鬆地分析它們。'; case 'backup.export.description': return '以不同格式下載資料'; case 'backup.export.dialog_title': return '儲存/傳送文件'; - case 'backup.export.success': return ({required Object x}) => '檔案已在 ${x} 中成功儲存/下載'; + case 'backup.export.success': return ({required Object x}) => '檔案已在${x}中成功儲存/下載'; case 'backup.export.error': return '下載檔案時發生錯誤。請透過 lozin.technologies@gmail.com 聯絡開發人員'; case 'backup.import.title': return '匯入您的資料'; - case 'backup.import.title_short': return '進口'; + case 'backup.import.title_short': return '匯入'; case 'backup.import.restore_backup': return '恢復備份'; case 'backup.import.restore_backup_descr': return '從 Monekin 匯入先前儲存的資料庫。此操作將用新資料取代任何當前應用程式資料'; - case 'backup.import.restore_backup_warn_description': return '導入新資料庫時,您將丟失應用程式中當前保存的所有資料。建議在繼續之前進行備份。請勿在此處上傳任何來源不明的文件,僅上傳您之前從其下載的文件莫尼金'; + case 'backup.import.restore_backup_warn_description': return '導入新資料庫時,您將丟失應用程式中當前保存的所有資料。建議在繼續之前進行備份。請勿在此處上傳任何來源不明的文件,僅上傳您之前從其下載的文件 Monekin'; case 'backup.import.restore_backup_warn_title': return '覆蓋所有數據'; case 'backup.import.select_other_file': return '選擇其他文件'; case 'backup.import.tap_to_select_file': return '點選選擇檔案'; @@ -7666,12 +7761,12 @@ extension on _TranslationsZhTw { case 'backup.import.manual_import.steps.4': return '日期欄位'; case 'backup.import.manual_import.steps.5': return '其他欄位'; case 'backup.import.manual_import.steps_descr.0': return '從您的裝置中選擇一個 .csv 檔案。確保它的第一行描述了每列的名稱。'; - case 'backup.import.manual_import.steps_descr.1': return '選擇指定每筆交易價值的欄位。使用負值表示支出,使用正值表示收入。使用點作為小數點分隔符號。'; - case 'backup.import.manual_import.steps_descr.2': return '選擇指定每筆交易所屬帳戶的列。您也可以選擇預設帳戶,以防我們找不到您想要的帳戶。如果未指定預設帳戶,我們將建立一個同名帳戶。'; - case 'backup.import.manual_import.steps_descr.3': return '指定交易類別名稱所在的欄位。您必須指定一個預設類別,以便我們將此類別指派給交易,以防找不到該類別。'; - case 'backup.import.manual_import.steps_descr.4': return '選擇指定每筆交易日期的欄位。如果未指定,交易將使用當前日期建立。'; - case 'backup.import.manual_import.steps_descr.5': return 'Specifies the columns for other optional transaction attributes'; - case 'backup.import.manual_import.success': return ({required Object x}) => '已成功導入 ${x} 筆交易'; + case 'backup.import.manual_import.steps_descr.1': return '選擇每筆交易價值的欄位。使用負值表示支出,使用正值表示收入。使用點作為小數點分隔符號。'; + case 'backup.import.manual_import.steps_descr.2': return '選擇每筆交易所屬帳戶的列。您也可以選擇預設帳戶,以防我們找不到您想要的帳戶。如果未指定預設帳戶,我們將建立一個同名帳戶。'; + case 'backup.import.manual_import.steps_descr.3': return '選擇交易類別名稱所在的欄位。您必須指定一個預設類別,以便我們將此類別指派給交易,以防找不到該類別。'; + case 'backup.import.manual_import.steps_descr.4': return '選擇每筆交易日期的欄位。如果未指定,交易將使用當前日期建立。'; + case 'backup.import.manual_import.steps_descr.5': return '選擇其他交易屬性的資料列'; + case 'backup.import.manual_import.success': return ({required Object x}) => '已成功導入${x}筆交易'; case 'backup.import.success': return '導入成功'; case 'backup.import.cancelled': return '導入已被用戶取消'; case 'backup.import.error': return '匯入檔案時發生錯誤。請透過 lozin.technologies@gmail.com 聯絡開發人員。'; @@ -7679,7 +7774,7 @@ extension on _TranslationsZhTw { case 'backup.about.create_date': return '創建日期'; case 'backup.about.modify_date': return '上一次更改'; case 'backup.about.last_backup': return '上次備份'; - case 'backup.about.size': return '尺寸'; + case 'backup.about.size': return '檔案大小'; case 'settings.title_long': return '設定和外觀'; case 'settings.title_short': return '設定'; case 'settings.description': return '應用程式主題、文字和其他常規設定'; @@ -7694,8 +7789,8 @@ extension on _TranslationsZhTw { case 'settings.theme_and_colors': return '主題和顏色'; case 'settings.theme': return '主題'; case 'settings.theme_auto': return '由系統定義'; - case 'settings.theme_light': return '明亮'; - case 'settings.theme_dark': return '黑暗'; + case 'settings.theme_light': return '明亮主題'; + case 'settings.theme_dark': return '黑暗主題'; case 'settings.amoled_mode': return 'amoled mode'; case 'settings.amoled_mode_descr': return '盡可能使用純黑色壁紙。這對 AMOLED 螢幕設備的電池略有幫助'; case 'settings.dynamic_colors': return '動態色彩'; @@ -7703,18 +7798,18 @@ extension on _TranslationsZhTw { case 'settings.accent_color': return '強調色'; case 'settings.accent_color_descr': return '選擇應用程式用來強調介面某些部分的顏色'; case 'settings.security.title': return '安全'; - case 'settings.security.private_mode_at_launch': return '启动时的私密模式'; - case 'settings.security.private_mode_at_launch_descr': return '默认以私有模式启动应用程序'; - case 'settings.security.private_mode': return '私人模式'; - case 'settings.security.private_mode_descr': return '隐藏所有货币值'; - case 'settings.security.private_mode_activated': return '隐私模式已激活'; - case 'settings.security.private_mode_deactivated': return '私人模式已禁用'; + case 'settings.security.private_mode_at_launch': return '啟動時啟用的私密模式'; + case 'settings.security.private_mode_at_launch_descr': return '默認以私密模式啟動應用程序'; + case 'settings.security.private_mode': return '私密模式'; + case 'settings.security.private_mode_descr': return '隱藏所有貨幣值'; + case 'settings.security.private_mode_activated': return '私密模式已啟用'; + case 'settings.security.private_mode_deactivated': return '私密模式已禁用'; case 'more.title': return '更多的'; case 'more.title_long': return '更多操作'; case 'more.data.display': return '數據'; case 'more.data.display_descr': return '匯出和匯入您的數據,這樣您就不會丟失任何東西'; case 'more.data.delete_all': return '刪除我的資料'; - case 'more.data.delete_all_header1': return '學徒就停在那裡⚠️⚠️'; + case 'more.data.delete_all_header1': return 'Stop right there padawan ⚠️⚠️'; case 'more.data.delete_all_message1': return '您確定要繼續嗎?您的所有資料將永久刪除且無法恢復'; case 'more.data.delete_all_header2': return '最後一步⚠️⚠️'; case 'more.data.delete_all_message2': return '刪除帳戶後,您將刪除所有儲存的個人資料。您的帳戶、交易、預算和類別將被刪除且無法恢復。您同意嗎?'; @@ -7726,13 +7821,13 @@ extension on _TranslationsZhTw { case 'more.about_us.legal.licenses': return '許可證'; case 'more.about_us.project.display': return '專案'; case 'more.about_us.project.contributors': return '合作者'; - case 'more.about_us.project.contributors_descr': return '所有讓莫尼金成長的開發者'; + case 'more.about_us.project.contributors_descr': return '所有讓 Monekin 成長的開發者'; case 'more.about_us.project.contact': return '聯絡我們'; case 'more.help_us.display': return '幫助我們'; - case 'more.help_us.description': return '了解如何幫助莫尼金變得越來越好'; + case 'more.help_us.description': return '了解如何幫助 Monekin 變得越來越好'; case 'more.help_us.rate_us': return '評價我們'; case 'more.help_us.rate_us_descr': return '歡迎任何價格!'; - case 'more.help_us.share': return '分享莫尼金'; + case 'more.help_us.share': return '分享 Monekin'; case 'more.help_us.share_descr': return '與朋友和家人分享我們的應用程式'; case 'more.help_us.share_text': return 'Monekin!最好的個人理財應用程式。在這裡下載'; case 'more.help_us.thanks': return '謝謝你!'; diff --git a/pubspec.lock b/pubspec.lock index 15fc81e5..a84df098 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -333,10 +333,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb + sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" url: "https://pub.dev" source: hosted - version: "0.68.0" + version: "0.69.0" flutter: dependency: "direct main" description: flutter @@ -601,18 +601,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -649,18 +649,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" mime: dependency: transitive description: @@ -905,10 +905,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -918,10 +918,10 @@ packages: dependency: "direct main" description: name: slang - sha256: f68f6d6709890f85efabfb0318e9d694be2ebdd333e57fe5cb50eee449e4e3ab + sha256: a2f704508bf9f209b71c881347bd27de45309651e9bd63570e4dd6ed2a77fbd2 url: "https://pub.dev" source: hosted - version: "3.31.1" + version: "3.31.2" slang_build_runner: dependency: "direct dev" description: @@ -1038,10 +1038,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.0" timing: dependency: transitive description: @@ -1126,10 +1126,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -1166,10 +1166,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.1" watcher: dependency: transitive description: @@ -1186,22 +1186,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" - url: "https://pub.dev" - source: hosted - version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "2.4.5" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 79e11ddd..57337b70 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: 'none' # The X represents the major releases, and can be up to infinity. Never start by a zero # The Y represents the minor releases, and can be up to 99. Always start by a zero # The Z represents the patch releases, and can be up to 999. Always start by a zero -version: 7.0.1+700001 +version: 7.1.0+710000 environment: flutter: 3.22.3 @@ -36,7 +36,7 @@ dependencies: # i18n intl: ^0.19.0 - slang: ^3.30.1 + slang: ^3.31.2 slang_flutter: ^3.30.1 # Drift @@ -50,11 +50,11 @@ dependencies: flutter_svg: ^2.0.5 permission_handler: ^11.3.0 json_annotation: ^4.8.1 - fl_chart: ^0.68.0 + fl_chart: ^0.69.0 file_picker: ^8.0.7 share_plus: ^9.0.0 url_launcher: ^6.1.11 - uuid: ^4.4.2 + uuid: ^4.5.1 csv: ^6.0.0 dotted_border: ^2.0.0+3 package_info_plus: ^8.0.2 diff --git a/test/evaluate_expression_test.dart b/test/evaluate_expression_test.dart new file mode 100644 index 00000000..4b1917cc --- /dev/null +++ b/test/evaluate_expression_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:monekin/app/transactions/form/dialogs/evaluate_expression.dart'; + +void main() { + group('evaluateExpression tests', () { + test('Single number', () { + expect(evaluateExpression('34'), equals(34.0)); + }); + + test('Single negative number', () { + expect(evaluateExpression('-34'), equals(-34.0)); + }); + + test('Simple addition', () { + expect(evaluateExpression('34 + 3'), equals(37.0)); + }); + + test('Simple addition without spaces', () { + expect(evaluateExpression('34+3'), equals(37.0)); + }); + + test('Multiple operations with spaces', () { + expect(evaluateExpression('24 + 4 - 2'), equals(26.0)); + }); + + test('Simple division', () { + expect(evaluateExpression('22/2'), equals(11.0)); + }); + + test('Expression with leading negative number', () { + expect(evaluateExpression('-3 + 4'), equals(1.0)); + }); + + test('Trailing operator is ignored', () { + expect(evaluateExpression('34+'), equals(34.0)); + }); + + test('Trailing operator with multiple operations', () { + expect(evaluateExpression('22/2*'), equals(11.0)); + }); + + test('Complex expression with negative and positive numbers', () { + expect(evaluateExpression('-3 + 4 - 5'), equals(-4.0)); + }); + + test('Multiple operations without spaces', () { + expect(evaluateExpression('2*3+4/2'), equals(8.0)); + }); + + test('Expression with floating point numbers', () { + expect(evaluateExpression('10.5 + 2.5'), equals(13.0)); + }); + + test('Expression with multiple operators in sequence', () { + expect(evaluateExpression('3 * 2 / 3'), equals(2.0)); + }); + + test('Invalid expression with only operators', () { + expect(() => evaluateExpression('+'), throwsArgumentError); + }); + }); +}