From 604b29745adc0868c6e7b29415ec4d2d4c6e9a13 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sat, 16 Nov 2024 18:38:41 +0800 Subject: [PATCH 01/19] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index aea210d..8997f93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.8.1+79" +version: "0.8.1+80" environment: sdk: ">=3.5.0 <4.0.0" From cc6656570a1c040c267ebb3a5be2aeb9fca182a5 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Mon, 18 Nov 2024 13:28:41 +0800 Subject: [PATCH 02/19] fix: stats_tab data ignoring when isPending is not present --- lib/objectbox/actions.dart | 28 +++++++++++++++------------- pubspec.yaml | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index 60ce6fc..40b325f 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -144,13 +144,14 @@ extension MainActions on ObjectBox { bool ignoreTransfers = true, String? currencyOverride, }) async { - final Condition dateFilter = - Transaction_.transactionDate.betweenDate(from, to); + final Condition dateFilter = Transaction_.transactionDate + .betweenDate(from, to) + .and(Transaction_.isPending + .isNull() + .or(Transaction_.isPending.notEquals(true))); - final Query transactionsQuery = ObjectBox() - .box() - .query(dateFilter.and(Transaction_.isPending.notEquals(true))) - .build(); + final Query transactionsQuery = + ObjectBox().box().query(dateFilter).build(); final List transactions = await transactionsQuery.findAsync(); @@ -181,13 +182,14 @@ extension MainActions on ObjectBox { bool omitZeroes = true, String? currencyOverride, }) async { - final Condition dateFilter = - Transaction_.transactionDate.betweenDate(from, to); - - final Query transactionsQuery = ObjectBox() - .box() - .query(dateFilter.and(Transaction_.isPending.notEquals(true))) - .build(); + final Condition dateFilter = Transaction_.transactionDate + .betweenDate(from, to) + .and(Transaction_.isPending + .isNull() + .or(Transaction_.isPending.notEquals(true))); + + final Query transactionsQuery = + ObjectBox().box().query(dateFilter).build(); final List transactions = await transactionsQuery.findAsync(); diff --git a/pubspec.yaml b/pubspec.yaml index 8997f93..a70f064 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.8.1+80" +version: "0.8.1+81" environment: sdk: ">=3.5.0 <4.0.0" From 7bf981f2c90024e2cc6679f2d8335a2c1e5e3814 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sat, 23 Nov 2024 15:47:26 +0800 Subject: [PATCH 03/19] chore: rename .money => .formatted --- lib/data/money.dart | 67 ++++++++ lib/l10n/extensions.dart | 63 ------- lib/routes/account_page.dart | 3 +- lib/routes/category_page.dart | 3 +- lib/routes/home/home_tab.dart | 7 +- lib/routes/transactions_page.dart | 29 +++- lib/widgets/category/transactions_info.dart | 2 +- lib/widgets/category_card.dart | 3 +- lib/widgets/flow_card.dart | 3 +- lib/widgets/general/money_text.dart | 138 ++++----------- lib/widgets/general/money_text_builder.dart | 160 ++++++++++++++++++ lib/widgets/general/money_text_raw.dart | 64 +++++++ lib/widgets/grouped_transaction_list.dart | 47 +---- .../pending_group_header.dart | 48 ++++++ .../home/transactions_date_header.dart | 80 +++++++-- .../setup/accounts/account_preset_card.dart | 4 +- 16 files changed, 477 insertions(+), 244 deletions(-) create mode 100644 lib/widgets/general/money_text_builder.dart create mode 100644 lib/widgets/general/money_text_raw.dart create mode 100644 lib/widgets/grouped_transaction_list/pending_group_header.dart diff --git a/lib/data/money.dart b/lib/data/money.dart index 4455287..dc7e976 100644 --- a/lib/data/money.dart +++ b/lib/data/money.dart @@ -2,6 +2,7 @@ import "dart:developer"; import "package:flow/data/currencies.dart"; import "package:flow/data/exchange_rates.dart"; +import "package:intl/intl.dart"; class Money { final double amount; @@ -138,6 +139,72 @@ class Money { toString() { return "Money($currency $amount)"; } + + String formatMoney({ + bool includeCurrency = true, + bool useCurrencySymbol = true, + bool compact = false, + bool takeAbsoluteValue = false, + int? decimalDigits, + }) { + final num amountToFormat = takeAbsoluteValue ? amount.abs() : amount; + final String currencyToFormat = !includeCurrency ? "" : currency; + useCurrencySymbol = useCurrencySymbol && includeCurrency; + + final String? symbol = useCurrencySymbol + ? NumberFormat.simpleCurrency( + locale: Intl.defaultLocale, + name: currencyToFormat, + ).currencySymbol + : null; + + if (compact) { + return NumberFormat.compactCurrency( + locale: Intl.defaultLocale, + name: currencyToFormat, + symbol: symbol, + decimalDigits: decimalDigits, + ).format(amountToFormat); + } + return NumberFormat.currency( + locale: Intl.defaultLocale, + name: currencyToFormat, + symbol: symbol, + decimalDigits: decimalDigits, + ).format(amountToFormat); + } + + /// Returns money-formatted string in the primary currency + /// in the default locale + /// + /// e.g., $420.69 + String get formatted => formatMoney(); + + /// Returns compact money-formatted string in the primary + /// currency in the default locale + /// + /// e.g., $1.2M + String get formattedCompact => formatMoney(compact: true); + + /// Returns money-formatted string (in the default locale) + /// + /// e.g., 467,000 + String get formattedNoMarker => formatMoney(includeCurrency: false); + + /// Returns money-formatted string (in the default locale) + /// + /// e.g., 1.2M + String get formattedNoMarkerCompact => formatMoney( + includeCurrency: false, + compact: true, + ); + + String toSemanticLabel() { + final String currencyName = + iso4217CurrenciesGrouped[currency]?.name ?? currency; + + return "$formattedNoMarker $currencyName"; + } } class MoneyException implements Exception { diff --git a/lib/l10n/extensions.dart b/lib/l10n/extensions.dart index b1b7c8d..986d997 100644 --- a/lib/l10n/extensions.dart +++ b/lib/l10n/extensions.dart @@ -1,7 +1,5 @@ -import "package:flow/data/money.dart"; import "package:flow/l10n/flow_localizations.dart"; import "package:flutter/widgets.dart"; -import "package:intl/intl.dart"; extension L10nHelper on BuildContext { FlowLocalizations get l => FlowLocalizations.of(this); @@ -44,64 +42,3 @@ extension L10nStringHelper on String { String tr([dynamic replace]) => FlowLocalizations.getTransalation(this, replace: replace); } - -extension MoneyFormatters on Money { - String formatMoney({ - bool includeCurrency = true, - bool useCurrencySymbol = true, - bool compact = false, - bool takeAbsoluteValue = false, - int? decimalDigits, - }) { - final num amountToFormat = takeAbsoluteValue ? amount.abs() : amount; - final String currencyToFormat = !includeCurrency ? "" : currency; - useCurrencySymbol = useCurrencySymbol && includeCurrency; - - final String? symbol = useCurrencySymbol - ? NumberFormat.simpleCurrency( - locale: Intl.defaultLocale, - name: currencyToFormat, - ).currencySymbol - : null; - - if (compact) { - return NumberFormat.compactCurrency( - locale: Intl.defaultLocale, - name: currencyToFormat, - symbol: symbol, - decimalDigits: decimalDigits, - ).format(amountToFormat); - } - return NumberFormat.currency( - locale: Intl.defaultLocale, - name: currencyToFormat, - symbol: symbol, - decimalDigits: decimalDigits, - ).format(amountToFormat); - } - - /// Returns money-formatted string in the primary currency - /// in the default locale - /// - /// e.g., $420.69 - String get money => formatMoney(); - - /// Returns compact money-formatted string in the primary - /// currency in the default locale - /// - /// e.g., $1.2M - String get moneyCompact => formatMoney(compact: true); - - /// Returns money-formatted string (in the default locale) - /// - /// e.g., 467,000 - String get moneyNoMarker => formatMoney(includeCurrency: false); - - /// Returns money-formatted string (in the default locale) - /// - /// e.g., 1.2M - String get moneyNoMarkerCompact => formatMoney( - includeCurrency: false, - compact: true, - ); -} diff --git a/lib/routes/account_page.dart b/lib/routes/account_page.dart index eb57f6b..cf0ba1b 100644 --- a/lib/routes/account_page.dart +++ b/lib/routes/account_page.dart @@ -173,10 +173,11 @@ class _AccountPageState extends State { listPadding: widget.listPadding, headerPadding: widget.headerPadding, firstHeaderTopPadding: firstHeaderTopPadding, - headerBuilder: (range, rangeTransactions) => + headerBuilder: (pendingGroup, range, rangeTransactions) => TransactionListDateHeader( transactions: rangeTransactions, date: range.from, + pendingGroup: pendingGroup, ), ) }, diff --git a/lib/routes/category_page.dart b/lib/routes/category_page.dart index abf0662..1faaa2f 100644 --- a/lib/routes/category_page.dart +++ b/lib/routes/category_page.dart @@ -189,10 +189,11 @@ class _CategoryPageState extends State { listPadding: widget.listPadding, headerPadding: widget.headerPadding, firstHeaderTopPadding: firstHeaderTopPadding, - headerBuilder: (range, rangeTransactions) => + headerBuilder: (pendingGroup, range, rangeTransactions) => TransactionListDateHeader( transactions: rangeTransactions, date: range.from, + pendingGroup: pendingGroup, ), ) }, diff --git a/lib/routes/home/home_tab.dart b/lib/routes/home/home_tab.dart index 2fe537c..bae937c 100644 --- a/lib/routes/home/home_tab.dart +++ b/lib/routes/home/home_tab.dart @@ -187,13 +187,14 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { bottom: 80.0, ), headerBuilder: ( - TimeRange range, - List transactions, + pendingGroup, + range, + transactions, ) => TransactionListDateHeader( transactions: transactions, date: range.from, - future: !range.from.isPast, + pendingGroup: pendingGroup == true, ), ); } diff --git a/lib/routes/transactions_page.dart b/lib/routes/transactions_page.dart index 335871b..7b08208 100644 --- a/lib/routes/transactions_page.dart +++ b/lib/routes/transactions_page.dart @@ -3,6 +3,7 @@ import "package:flow/objectbox.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/objectbox/objectbox.g.dart"; import "package:flow/widgets/general/spinner.dart"; +import "package:flow/widgets/general/wavy_divider.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; import "package:flow/widgets/home/transactions_date_header.dart"; import "package:flutter/material.dart"; @@ -91,25 +92,41 @@ class _TransactionsPageState extends State { title: widget.title == null ? null : Text(widget.title!), ), body: SafeArea( - child: StreamBuilder>>( + child: StreamBuilder>( stream: widget.query .watch(triggerImmediately: true) - .map((event) => event.find().groupByDate()), + .map((event) => event.find()), builder: (context, snapshot) { if (!snapshot.hasData) { return const Spinner.center(); } - final Map> grouped = - snapshot.requireData; + final DateTime now = DateTime.now().startOfNextMinute(); + + final Map> transactions = snapshot + .requireData + .where((transaction) => + !transaction.transactionDate.isAfter(now) && + transaction.isPending != true) + .groupByDate(); + final Map> pendingTransactions = + snapshot + .requireData + .where((transaction) => + transaction.transactionDate.isAfter(now) || + transaction.isPending == true) + .groupByDate(); return GroupedTransactionList( - transactions: grouped, - headerBuilder: (range, transactions) => + transactions: transactions, + pendingTransactions: pendingTransactions, + headerBuilder: (pendingGroup, range, transactions) => TransactionListDateHeader( + pendingGroup: pendingGroup, transactions: transactions, date: range.from, ), + pendingDivider: WavyDivider(), header: widget.header, ); }, diff --git a/lib/widgets/category/transactions_info.dart b/lib/widgets/category/transactions_info.dart index 7e302c7..94a0b0d 100644 --- a/lib/widgets/category/transactions_info.dart +++ b/lib/widgets/category/transactions_info.dart @@ -34,7 +34,7 @@ class TransactionsInfo extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - flow.money, + flow.formatted, style: context.textTheme.displaySmall, ), Text( diff --git a/lib/widgets/category_card.dart b/lib/widgets/category_card.dart index abb97a6..654b5ad 100644 --- a/lib/widgets/category_card.dart +++ b/lib/widgets/category_card.dart @@ -1,6 +1,5 @@ import "package:flow/data/money.dart"; import "package:flow/entity/category.dart"; -import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; import "package:flow/theme/theme.dart"; @@ -61,7 +60,7 @@ class CategoryCard extends StatelessWidget { Text( Money(category.transactions.sumWithoutCurrency, primaryCurrency) - .money, + .formatted, style: context.textTheme.bodyMedium?.semi(context), ), ], diff --git a/lib/widgets/flow_card.dart b/lib/widgets/flow_card.dart index df9dfa0..12b8972 100644 --- a/lib/widgets/flow_card.dart +++ b/lib/widgets/flow_card.dart @@ -1,7 +1,6 @@ import "package:auto_size_text/auto_size_text.dart"; import "package:flow/data/money.dart"; import "package:flow/entity/transaction.dart"; -import "package:flow/l10n/extensions.dart"; import "package:flow/theme/theme.dart"; import "package:flow/widgets/general/surface.dart"; import "package:flutter/material.dart"; @@ -32,7 +31,7 @@ class FlowCard extends StatelessWidget { alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), child: AutoSizeText( - flow.abs().money, + flow.abs().formatted, style: context.textTheme.displaySmall?.copyWith( color: type.color(context), ), diff --git a/lib/widgets/general/money_text.dart b/lib/widgets/general/money_text.dart index 33a6f15..4627dd0 100644 --- a/lib/widgets/general/money_text.dart +++ b/lib/widgets/general/money_text.dart @@ -1,8 +1,7 @@ import "package:auto_size_text/auto_size_text.dart"; import "package:flow/data/money.dart"; -import "package:flow/l10n/extensions.dart"; -import "package:flow/prefs.dart"; -import "package:flow/utils/utils.dart"; +import "package:flow/widgets/general/money_text_builder.dart"; +import "package:flow/widgets/general/money_text_raw.dart"; import "package:flutter/material.dart"; class MoneyText extends StatefulWidget { @@ -77,128 +76,51 @@ class MoneyText extends StatefulWidget { } class _MoneyTextState extends State { - late bool globalPrivacyMode; - late bool globalUseCurrencySymbol; late bool abbreviate; - AutoSizeGroup? autoSizeGroup; @override - void initState() { + initState() { super.initState(); - LocalPreferences().sessionPrivacyMode.addListener(_privacyModeUpdate); - LocalPreferences().useCurrencySymbol.addListener(_useCurrencySymbolUpdate); - - globalPrivacyMode = LocalPreferences().sessionPrivacyMode.get(); - globalUseCurrencySymbol = LocalPreferences().useCurrencySymbol.get(); - abbreviate = widget.initiallyAbbreviated; - autoSizeGroup = widget.autoSizeGroup; - } - - @override - void didUpdateWidget(MoneyText oldWidget) { - if (widget.autoSizeGroup != autoSizeGroup) { - autoSizeGroup = widget.autoSizeGroup; - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - LocalPreferences().sessionPrivacyMode.removeListener(_privacyModeUpdate); - LocalPreferences() - .useCurrencySymbol - .removeListener(_useCurrencySymbolUpdate); - - super.dispose(); } @override Widget build(BuildContext context) { - final String text = getString(); - - final Widget child = widget.autoSize - ? AutoSizeText( - text, - group: autoSizeGroup, - style: widget.style, - maxLines: widget.maxLines, - textAlign: widget.textAlign, - ) - : Text( - text, - style: widget.style, - maxLines: widget.maxLines, - textAlign: widget.textAlign, - ); - - if (widget.tapToToggleAbbreviation || widget.onTap != null) { - return GestureDetector( - onTap: handleTap, - child: child, - ); - } - - return child; + return MoneyTextBuilder( + money: widget.money, + customFormatter: widget.customFormatter, + abbreviate: abbreviate, + overrideObscure: widget.overrideObscure, + overrideUseCurrencySymbol: widget.overrideUseCurrencySymbol, + displayAbsoluteAmount: widget.displayAbsoluteAmount, + omitCurrency: widget.omitCurrency, + builder: (context, text, money) { + final bool hasAction = + widget.onTap != null || widget.tapToToggleAbbreviation; + + return MoneyTextRaw( + text: text, + style: widget.style, + textAlign: widget.textAlign, + maxLines: widget.maxLines, + onTap: hasAction ? () => handleTap() : null, + autoSizeGroup: widget.autoSizeGroup, + autoSize: widget.autoSize, + ); + }, + ); } void handleTap() { - if (widget.onTap != null) { - widget.onTap!(); - } - if (widget.tapToToggleAbbreviation) { - setState(() { - abbreviate = !abbreviate; - }); - } - } - - _privacyModeUpdate() { - globalPrivacyMode = LocalPreferences().sessionPrivacyMode.get(); - if (!mounted) return; - setState(() {}); - } - - _useCurrencySymbolUpdate() { - globalUseCurrencySymbol = LocalPreferences().useCurrencySymbol.get(); - if (!mounted) return; - setState(() {}); - } - - String getString() { - final Money? money = widget.money; + abbreviate = !abbreviate; - if (money == null) return "-"; - - final bool obscure = widget.overrideObscure ?? globalPrivacyMode; - - final bool useCurrencySymbol = - widget.overrideUseCurrencySymbol ?? globalUseCurrencySymbol; - - if (widget.customFormatter != null) { - return widget.customFormatter!( - money, - ( - abbreviate: abbreviate, - obscure: obscure, - useCurrencySymbol: useCurrencySymbol - ), - ); + if (mounted) setState(() => {}); } - final String text = money.formatMoney( - compact: abbreviate, - takeAbsoluteValue: widget.displayAbsoluteAmount, - includeCurrency: !widget.omitCurrency, - useCurrencySymbol: useCurrencySymbol, - ); - - if (obscure) { - return text.digitsObscured; + if (widget.onTap != null) { + widget.onTap!(); } - - return text; } } diff --git a/lib/widgets/general/money_text_builder.dart b/lib/widgets/general/money_text_builder.dart new file mode 100644 index 0000000..d7b18d6 --- /dev/null +++ b/lib/widgets/general/money_text_builder.dart @@ -0,0 +1,160 @@ +import "package:flow/data/money.dart"; +import "package:flow/prefs.dart"; +import "package:flow/utils/utils.dart"; +import "package:flutter/material.dart"; + +class MoneyTextBuilder extends StatefulWidget { + final Function(BuildContext, String, Money?)? builder; + + final Money? money; + + final String Function( + Money money, + ({ + bool abbreviate, + bool obscure, + bool useCurrencySymbol, + }) options, + )? customFormatter; + + /// Defaults to [false] + final bool abbreviate; + + final bool displayAbsoluteAmount; + final bool omitCurrency; + + /// Uses 3-letter-code instead of the currency symbol. + /// + /// e.g., '€' instead of 'EUR' + final bool? overrideUseCurrencySymbol; + + /// Set this to [true] to make it always unobscured + /// + /// Set this to [false] to make it always obscured + /// + /// Set this to [null] to use the default behavior + final bool? overrideObscure; + + const MoneyTextBuilder({ + super.key, + required this.builder, + required this.money, + this.abbreviate = false, + this.displayAbsoluteAmount = false, + this.omitCurrency = false, + this.overrideUseCurrencySymbol, + this.overrideObscure, + this.customFormatter, + }); + + @override + State createState() => _MoneyTextBuilderState(); +} + +class _MoneyTextBuilderState extends State { + late Money? money; + + late bool? overrideObscure; + late bool? overrideUseCurrencySymbol; + + late bool globalPrivacyMode; + late bool globalUseCurrencySymbol; + late bool abbreviate; + + @override + void initState() { + super.initState(); + + money = widget.money; + overrideObscure = widget.overrideObscure; + overrideUseCurrencySymbol = widget.overrideUseCurrencySymbol; + abbreviate = widget.abbreviate; + + LocalPreferences().sessionPrivacyMode.addListener(_privacyModeUpdate); + LocalPreferences().useCurrencySymbol.addListener(_useCurrencySymbolUpdate); + + globalPrivacyMode = LocalPreferences().sessionPrivacyMode.get(); + globalUseCurrencySymbol = LocalPreferences().useCurrencySymbol.get(); + } + + @override + void didUpdateWidget(MoneyTextBuilder oldWidget) { + if (oldWidget.money != widget.money) { + money = widget.money; + } + if (oldWidget.overrideObscure != widget.overrideObscure) { + overrideObscure = widget.overrideObscure; + } + if (oldWidget.overrideUseCurrencySymbol != widget.overrideObscure) { + overrideUseCurrencySymbol = widget.overrideUseCurrencySymbol; + } + if (oldWidget.abbreviate != widget.abbreviate) { + abbreviate = widget.abbreviate; + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + LocalPreferences().sessionPrivacyMode.removeListener(_privacyModeUpdate); + LocalPreferences() + .useCurrencySymbol + .removeListener(_useCurrencySymbolUpdate); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final String text = getString(); + + return widget.builder!(context, text, widget.money); + } + + _privacyModeUpdate() { + globalPrivacyMode = LocalPreferences().sessionPrivacyMode.get(); + if (!mounted) return; + setState(() {}); + } + + _useCurrencySymbolUpdate() { + globalUseCurrencySymbol = LocalPreferences().useCurrencySymbol.get(); + if (!mounted) return; + setState(() {}); + } + + String getString() { + final Money? money = widget.money; + + if (money == null) return "-"; + + final bool obscure = overrideObscure ?? globalPrivacyMode; + + final bool useCurrencySymbol = + overrideUseCurrencySymbol ?? globalUseCurrencySymbol; + + if (widget.customFormatter != null) { + return widget.customFormatter!( + money, + ( + abbreviate: abbreviate, + obscure: obscure, + useCurrencySymbol: useCurrencySymbol + ), + ); + } + + final String text = money.formatMoney( + compact: abbreviate, + takeAbsoluteValue: widget.displayAbsoluteAmount, + includeCurrency: !widget.omitCurrency, + useCurrencySymbol: useCurrencySymbol, + ); + + if (obscure) { + return text.digitsObscured; + } + + return text; + } +} diff --git a/lib/widgets/general/money_text_raw.dart b/lib/widgets/general/money_text_raw.dart new file mode 100644 index 0000000..725a0db --- /dev/null +++ b/lib/widgets/general/money_text_raw.dart @@ -0,0 +1,64 @@ +import "package:auto_size_text/auto_size_text.dart"; +import "package:flow/data/money.dart"; +import "package:flutter/material.dart"; + +class MoneyTextRaw extends StatelessWidget { + final String text; + final Money? money; + + /// When true, renders [AutoSizeText] + /// + /// When false, renders [Text] + final bool autoSize; + + /// Pass an [AutoSizeGroup] to synchronize + /// fontSize among multiple [AutoSizeText]s + final AutoSizeGroup? autoSizeGroup; + + final int maxLines; + + final TextAlign? textAlign; + final TextStyle? style; + + final VoidCallback? onTap; + + const MoneyTextRaw({ + super.key, + this.money, + required this.text, + this.autoSize = false, + this.autoSizeGroup, + required this.maxLines, + this.textAlign, + this.style, + this.onTap, + }); + @override + Widget build(BuildContext context) { + final Widget child = autoSize + ? AutoSizeText( + text, + group: autoSizeGroup, + style: style, + maxLines: maxLines, + textAlign: textAlign, + semanticsLabel: money?.toSemanticLabel() ?? text, + ) + : Text( + text, + style: style, + maxLines: maxLines, + textAlign: textAlign, + semanticsLabel: money?.toSemanticLabel() ?? text, + ); + + if (onTap != null) { + return GestureDetector( + onTap: onTap, + child: child, + ); + } + + return child; + } +} diff --git a/lib/widgets/grouped_transaction_list.dart b/lib/widgets/grouped_transaction_list.dart index b98f067..8ebd120 100644 --- a/lib/widgets/grouped_transaction_list.dart +++ b/lib/widgets/grouped_transaction_list.dart @@ -3,11 +3,10 @@ import "package:flow/entity/transaction.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; -import "package:flow/theme/helpers.dart"; import "package:flow/utils/utils.dart"; +import "package:flow/widgets/grouped_transaction_list/pending_group_header.dart"; import "package:flow/widgets/transaction_list_tile.dart"; import "package:flutter/material.dart"; -import "package:go_router/go_router.dart"; import "package:moment_dart/moment_dart.dart"; class GroupedTransactionList extends StatefulWidget { @@ -26,8 +25,11 @@ class GroupedTransactionList extends StatefulWidget { /// Rendered in order. final Map>? pendingTransactions; - final Widget Function(TimeRange range, List transactions) - headerBuilder; + final Widget Function( + bool pendingGroup, + TimeRange range, + List transactions, + ) headerBuilder; /// Divider to displayed between future/past transactions. How it's divided /// is based on [anchor] @@ -107,15 +109,14 @@ class _GroupedTransactionListState extends State { final Widget? header = widget.header ?? (widget.implyHeader - ? _getImpliedHeader(context, - futureTransactions: widget.pendingTransactions) + ? PendingGroupHeader(futureTransactions: widget.pendingTransactions) : null); final List flattened = [ if (header != null) header, if (widget.pendingTransactions != null) for (final entry in widget.pendingTransactions!.entries) ...[ - widget.headerBuilder(entry.key, entry.value), + widget.headerBuilder(true, entry.key, entry.value), ...entry.value, ], if (widget.pendingDivider != null && @@ -123,7 +124,7 @@ class _GroupedTransactionListState extends State { widget.transactions.isNotEmpty) widget.pendingDivider!, for (final entry in widget.transactions.entries) ...[ - widget.headerBuilder(entry.key, entry.value), + widget.headerBuilder(false, entry.key, entry.value), ...entry.value, ], ]; @@ -182,36 +183,6 @@ class _GroupedTransactionListState extends State { transaction.confirm(confirm); } - Widget? _getImpliedHeader( - BuildContext context, { - required Map>? futureTransactions, - }) { - if (futureTransactions == null || futureTransactions.isEmpty) return null; - - final int count = futureTransactions.values.fold( - 0, - (previousValue, element) => previousValue + element.renderableCount, - ); - - return Row( - children: [ - Expanded( - child: Text( - "tabs.home.upcomingTransactions".t(context, count), - style: context.textTheme.bodyLarge?.semi(context), - ), - ), - const SizedBox(width: 16.0), - TextButton( - onPressed: () => context.push("/transactions/upcoming"), - child: Text( - "tabs.home.upcomingTransactions.seeAll".t(context), - ), - ) - ], - ); - } - _privacyModeUpdate() { globalPrivacyMode = LocalPreferences().sessionPrivacyMode.get(); if (!mounted) return; diff --git a/lib/widgets/grouped_transaction_list/pending_group_header.dart b/lib/widgets/grouped_transaction_list/pending_group_header.dart new file mode 100644 index 0000000..4e45abd --- /dev/null +++ b/lib/widgets/grouped_transaction_list/pending_group_header.dart @@ -0,0 +1,48 @@ +import "package:flow/entity/transaction.dart"; +import "package:flow/l10n/extensions.dart"; +import "package:flow/objectbox/actions.dart"; +import "package:flow/theme/theme.dart"; +import "package:flutter/material.dart"; +import "package:go_router/go_router.dart"; +import "package:moment_dart/moment_dart.dart"; + +class PendingGroupHeader extends StatelessWidget { + final Map>? futureTransactions; + + const PendingGroupHeader({ + super.key, + required this.futureTransactions, + }); + + @override + Widget build(BuildContext context) { + if (futureTransactions == null || futureTransactions!.isEmpty) { + return SizedBox.shrink(); + } + + final Map> transactions = futureTransactions!; + + final int count = transactions.values.fold( + 0, + (previousValue, element) => previousValue + element.renderableCount, + ); + + return Row( + children: [ + Expanded( + child: Text( + "tabs.home.upcomingTransactions".t(context, count), + style: context.textTheme.bodyLarge?.semi(context), + ), + ), + const SizedBox(width: 16.0), + TextButton( + onPressed: () => context.push("/transactions/upcoming"), + child: Text( + "tabs.home.upcomingTransactions.seeAll".t(context), + ), + ) + ], + ); + } +} diff --git a/lib/widgets/home/transactions_date_header.dart b/lib/widgets/home/transactions_date_header.dart index e7b1bc0..516d1c3 100644 --- a/lib/widgets/home/transactions_date_header.dart +++ b/lib/widgets/home/transactions_date_header.dart @@ -7,54 +7,102 @@ import "package:flow/theme/theme.dart"; import "package:flutter/widgets.dart"; import "package:moment_dart/moment_dart.dart"; -class TransactionListDateHeader extends StatelessWidget { +class TransactionListDateHeader extends StatefulWidget { final DateTime date; final List transactions; + final Widget? action; + /// Hides count and flow - final bool future; + final bool pendingGroup; const TransactionListDateHeader({ super.key, required this.transactions, required this.date, - this.future = false, + this.action, + this.pendingGroup = false, }); - const TransactionListDateHeader.future({ + const TransactionListDateHeader.pendingGroup({ super.key, required this.date, - }) : future = true, + this.action, + }) : pendingGroup = true, transactions = const []; + @override + State createState() => + _TransactionListDateHeaderState(); +} + +class _TransactionListDateHeaderState extends State { + bool obscure = false; + + @override + void initState() { + super.initState(); + + LocalPreferences().sessionPrivacyMode.addListener(_updatePrivacyMode); + obscure = LocalPreferences().sessionPrivacyMode.get(); + } + + @override + void dispose() { + LocalPreferences().sessionPrivacyMode.removeListener(_updatePrivacyMode); + + super.dispose(); + } + @override Widget build(BuildContext context) { final Widget title = Text( - date.toMoment().calendar(omitHours: true), + widget.date.toMoment().calendar(omitHours: true), style: context.textTheme.headlineSmall, ); - if (future) { + if (widget.pendingGroup) { return title; } final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); - final double flow = transactions + final double flow = widget.transactions .where((transaction) => transaction.currency == primaryCurrency) .sumWithoutCurrency; - final bool containsNonPrimaryCurrency = transactions + final bool containsNonPrimaryCurrency = widget.transactions .any((transaction) => transaction.currency != primaryCurrency); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + return Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - title, - Text( - "${Money(flow, primaryCurrency).moneyCompact}${containsNonPrimaryCurrency ? '+' : ''} • ${'tabs.home.transactionsCount'.t(context, transactions.renderableCount)}", - style: context.textTheme.labelMedium, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + title, + Text( + "${Money(flow, primaryCurrency).formattedCompact}${containsNonPrimaryCurrency ? '+' : ''} • ${'tabs.home.transactionsCount'.t(context, widget.transactions.renderableCount)}", + style: context.textTheme.labelMedium, + ), + ], + ), ), + const SizedBox(width: 16.0), + if (widget.action != null) + Flexible( + fit: FlexFit.tight, + child: widget.action!, + ), ], ); } + + _updatePrivacyMode() { + obscure = LocalPreferences().sessionPrivacyMode.get(); + + if (!mounted) return; + setState(() {}); + } } diff --git a/lib/widgets/setup/accounts/account_preset_card.dart b/lib/widgets/setup/accounts/account_preset_card.dart index 235bb36..05ee933 100644 --- a/lib/widgets/setup/accounts/account_preset_card.dart +++ b/lib/widgets/setup/accounts/account_preset_card.dart @@ -1,5 +1,4 @@ import "package:flow/entity/account.dart"; -import "package:flow/l10n/extensions.dart"; import "package:flow/theme/theme.dart"; import "package:flow/widgets/general/flow_icon.dart"; import "package:flow/widgets/general/surface.dart"; @@ -56,8 +55,7 @@ class AccountPresetCard extends StatelessWidget { style: context.textTheme.titleSmall, ), Text( - account.balance - .formatMoney(), + account.balance.formatMoney(), style: context.textTheme.displaySmall, ), ], From 84745f22449ecbaff9ba1309264c1f4a21079a47 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sat, 23 Nov 2024 17:20:00 +0800 Subject: [PATCH 04/19] Refactors, deprecate themeMode --- lib/data/transactions_filter.dart | 33 ++++++++++++++++ lib/main.dart | 30 ++------------ lib/prefs.dart | 7 ---- lib/routes/preferences_page.dart | 2 +- lib/theme/color_themes/registry.dart | 17 ++------ lib/theme/flow_color_scheme.dart | 2 + lib/theme/theme.dart | 17 +++++--- .../transaction_context_actions.dart | 28 +++++++++++++ lib/widgets/grouped_transaction_list.dart | 39 ++++--------------- ...dart => default_pending_group_header.dart} | 4 +- lib/widgets/theme_petal_selector.dart | 2 +- 11 files changed, 94 insertions(+), 87 deletions(-) create mode 100644 lib/utils/extensions/transaction_context_actions.dart rename lib/widgets/grouped_transaction_list/{pending_group_header.dart => default_pending_group_header.dart} (93%) diff --git a/lib/data/transactions_filter.dart b/lib/data/transactions_filter.dart index 4867cdd..43eb70b 100644 --- a/lib/data/transactions_filter.dart +++ b/lib/data/transactions_filter.dart @@ -36,11 +36,22 @@ class TransactionFilter { final bool sortDescending; final TransactionSortField sortBy; + final bool? isPending; + + final double? minAmount; + final double? maxAmount; + + final List? currencies; + const TransactionFilter({ this.categories, this.accounts, this.range, this.types, + this.isPending = false, + this.minAmount, + this.maxAmount, + this.currencies, this.sortDescending = true, this.searchData = const TransactionSearchData(), this.sortBy = TransactionSortField.transactionDate, @@ -125,6 +136,28 @@ class TransactionFilter { .oneOf(accounts!.map((account) => account.uuid).toList())); } + if (minAmount != null) { + conditions.add(Transaction_.amount.greaterOrEqual(minAmount!)); + } + + if (maxAmount != null) { + conditions.add(Transaction_.amount.lessOrEqual(maxAmount!)); + } + + if (currencies?.isNotEmpty == true) { + conditions.add(Transaction_.currency.oneOf(currencies!)); + } + + if (isPending != null) { + if (isPending!) { + conditions.add(Transaction_.isPending.equals(true)); + } else { + conditions.add(Transaction_.isPending + .notEquals(true) + .or(Transaction_.isPending.isNull())); + } + } + final filtered = ObjectBox() .box() .query(conditions.reduce((a, b) => a & b)); diff --git a/lib/main.dart b/lib/main.dart index 82f9e45..af196f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -155,38 +155,15 @@ class FlowState extends State { } void _reloadTheme() { - final ThemeMode legacyThemeMode = - LocalPreferences().themeMode.value ?? ThemeMode.system; final String? themeName = LocalPreferences().themeName.value; log("[Theme] Reloading theme $themeName"); - ({FlowColorScheme scheme, ThemeMode mode})? experimentalTheme = - getTheme(themeName); - - if (experimentalTheme == null) { - final bool fallbackToDarkTheme = - switch ((legacyThemeMode, useDarkTheme)) { - (ThemeMode.system, true) => true, - (ThemeMode.system, false) => true, - (ThemeMode.dark, _) => true, - (ThemeMode.light, _) => false - }; - - log("[Theme] Didn't find theme for $themeName"); - unawaited( - LocalPreferences() - .themeName - .set((fallbackToDarkTheme ? darkThemes : lightThemes).keys.first), - ); - } + FlowColorScheme theme = getTheme(themeName, useDarkTheme); setState(() { - _themeMode = experimentalTheme?.mode ?? legacyThemeMode; - _themeFactory = ThemeFactory( - experimentalTheme?.scheme ?? - (useDarkTheme ? electricLavender : shadeOfViolet), - ); + _themeMode = theme.mode; + _themeFactory = ThemeFactory(theme); }); } @@ -213,6 +190,7 @@ class FlowState extends State { MomentLocalizations.byLocale(overriddenLocale.code) ?? MomentLocalizations.enUS(), ); + Intl.defaultLocale = overriddenLocale.code; setState(() {}); } diff --git a/lib/prefs.dart b/lib/prefs.dart index 5098a3c..19b81b2 100644 --- a/lib/prefs.dart +++ b/lib/prefs.dart @@ -11,7 +11,6 @@ import "package:flow/entity/transaction.dart"; import "package:flow/objectbox.dart"; import "package:flow/objectbox/objectbox.g.dart"; import "package:flow/theme/color_themes/registry.dart"; -import "package:flutter/material.dart"; import "package:intl/intl.dart"; import "package:local_settings/local_settings.dart"; import "package:moment_dart/moment_dart.dart"; @@ -73,7 +72,6 @@ class LocalPreferences { late final BoolSettingsEntry autoAttachTransactionGeo; - late final ThemeModeSettingsEntry themeMode; late final PrimitiveSettingsEntry themeName; late final BoolSettingsEntry themeChangesAppIcon; late final BoolSettingsEntry enableDynamicTheme; @@ -175,11 +173,6 @@ class LocalPreferences { initialValue: false, ); - themeMode = ThemeModeSettingsEntry( - key: "themeMode", - preferences: _prefs, - initialValue: ThemeMode.system, - ); themeName = PrimitiveSettingsEntry( key: "themeName", preferences: _prefs, diff --git a/lib/routes/preferences_page.dart b/lib/routes/preferences_page.dart index e8a1663..a8f87cd 100644 --- a/lib/routes/preferences_page.dart +++ b/lib/routes/preferences_page.dart @@ -28,7 +28,7 @@ class _PreferencesPageState extends State { @override Widget build(BuildContext context) { final FlowColorScheme currentTheme = - getTheme(LocalPreferences().themeName.get())?.scheme ?? shadeOfViolet; + getTheme(LocalPreferences().themeName.get()); final UpcomingTransactionsDuration homeTabPlannedTransactionsDuration = LocalPreferences().homeTabPlannedTransactionsDuration.get() ?? diff --git a/lib/theme/color_themes/registry.dart b/lib/theme/color_themes/registry.dart index 6bf098a..2d45c47 100644 --- a/lib/theme/color_themes/registry.dart +++ b/lib/theme/color_themes/registry.dart @@ -5,7 +5,6 @@ import "package:flow/theme/color_themes/default_darks.dart"; import "package:flow/theme/color_themes/default_lights.dart"; import "package:flow/theme/color_themes/palenight.dart"; import "package:flow/theme/flow_color_scheme.dart"; -import "package:flutter/material.dart"; import "package:flutter_dynamic_icon_plus/flutter_dynamic_icon_plus.dart"; export "default_darks.dart"; @@ -98,25 +97,17 @@ bool validateThemeName(String? themeName) { return allThemes.containsKey(themeName); } -bool isThemeDark(String? themeName) { - final themeData = getTheme(themeName); - - return themeData?.mode == ThemeMode.dark; -} - -({FlowColorScheme scheme, ThemeMode mode})? getTheme(String? themeName) { - if (themeName == null) return null; +FlowColorScheme getTheme(String? themeName, [bool preferDark = false]) { + if (themeName == null) return preferDark ? electricLavender : shadeOfViolet; final FlowColorScheme? scheme = allThemes[themeName]; if (scheme == null) { log("Unknown theme: $themeName"); - return null; + return preferDark ? electricLavender : shadeOfViolet; } - final mode = scheme.isDark ? ThemeMode.dark : ThemeMode.light; - - return (scheme: scheme, mode: mode); + return scheme; } void trySetThemeIcon(String? name) async { diff --git a/lib/theme/flow_color_scheme.dart b/lib/theme/flow_color_scheme.dart index a4d766b..8a22a31 100644 --- a/lib/theme/flow_color_scheme.dart +++ b/lib/theme/flow_color_scheme.dart @@ -48,6 +48,8 @@ class FlowColorScheme { late final ColorScheme colorScheme; + ThemeMode get mode => isDark ? ThemeMode.dark : ThemeMode.light; + FlowColorScheme({ required this.isDark, required this.surface, diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 6221a87..54eccc7 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -153,11 +153,18 @@ class ThemeFactory { ); } - factory ThemeFactory.fromThemeName(String? themeName) { - final resolved = getTheme(themeName); - - if (resolved == null) return ThemeFactory(shadeOfViolet); + /// Returns a [ThemeFactory] instance based on the provided [themeName]. + /// + /// If [themeName] is `null`, the default theme is returned. + /// + /// Pass [preferDark] to influence the choice of default theme. + factory ThemeFactory.fromThemeName(String? themeName, + [bool preferDark = false]) { + final resolved = getTheme( + themeName, + preferDark, + ); - return ThemeFactory(resolved.scheme); + return ThemeFactory(resolved); } } diff --git a/lib/utils/extensions/transaction_context_actions.dart b/lib/utils/extensions/transaction_context_actions.dart new file mode 100644 index 0000000..8c0261c --- /dev/null +++ b/lib/utils/extensions/transaction_context_actions.dart @@ -0,0 +1,28 @@ +import "package:flow/entity/transaction.dart"; +import "package:flow/l10n/extensions.dart"; +import "package:flow/objectbox/actions.dart"; +import "package:flow/utils/utils.dart"; +import "package:flutter/widgets.dart"; + +extension TransactionContextActions on BuildContext { + Future deleteTransaction(Transaction transaction) async { + final String txnTitle = + transaction.title ?? "transaction.fallbackTitle".t(this); + + final confirmation = await showConfirmDialog( + isDeletionConfirmation: true, + title: "general.delete.confirmName".t(this, txnTitle), + ); + + if (confirmation == true) { + transaction.delete(); + } + } + + Future confirmTransaction( + Transaction transaction, [ + bool confirm = true, + ]) async { + transaction.confirm(confirm); + } +} diff --git a/lib/widgets/grouped_transaction_list.dart b/lib/widgets/grouped_transaction_list.dart index 8ebd120..4450967 100644 --- a/lib/widgets/grouped_transaction_list.dart +++ b/lib/widgets/grouped_transaction_list.dart @@ -1,10 +1,8 @@ import "package:flow/data/transactions_filter.dart"; import "package:flow/entity/transaction.dart"; -import "package:flow/l10n/extensions.dart"; -import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; -import "package:flow/utils/utils.dart"; -import "package:flow/widgets/grouped_transaction_list/pending_group_header.dart"; +import "package:flow/utils/extensions/transaction_context_actions.dart"; +import "package:flow/widgets/grouped_transaction_list/default_pending_group_header.dart"; import "package:flow/widgets/transaction_list_tile.dart"; import "package:flutter/material.dart"; import "package:moment_dart/moment_dart.dart"; @@ -109,7 +107,9 @@ class _GroupedTransactionListState extends State { final Widget? header = widget.header ?? (widget.implyHeader - ? PendingGroupHeader(futureTransactions: widget.pendingTransactions) + ? DefaultPendingGroupHeader( + futureTransactions: widget.pendingTransactions, + ) : null); final List flattened = [ @@ -147,9 +147,9 @@ class _GroupedTransactionListState extends State { transaction: transaction, padding: widget.itemPadding, dismissibleKey: ValueKey(transaction.id), - deleteFn: () => deleteTransaction(context, transaction), + deleteFn: () => context.deleteTransaction(transaction), confirmFn: ([bool confirm = true]) => - confirmTransaction(context, transaction, confirm), + context.confirmTransaction(transaction, confirm), overrideObscure: widget.overrideObscure, ), (_) => Container(), @@ -158,31 +158,6 @@ class _GroupedTransactionListState extends State { ); } - Future deleteTransaction( - BuildContext context, - Transaction transaction, - ) async { - final String txnTitle = - transaction.title ?? "transaction.fallbackTitle".t(context); - - final confirmation = await context.showConfirmDialog( - isDeletionConfirmation: true, - title: "general.delete.confirmName".t(context, txnTitle), - ); - - if (confirmation == true) { - transaction.delete(); - } - } - - Future confirmTransaction( - BuildContext context, - Transaction transaction, [ - bool confirm = true, - ]) async { - transaction.confirm(confirm); - } - _privacyModeUpdate() { globalPrivacyMode = LocalPreferences().sessionPrivacyMode.get(); if (!mounted) return; diff --git a/lib/widgets/grouped_transaction_list/pending_group_header.dart b/lib/widgets/grouped_transaction_list/default_pending_group_header.dart similarity index 93% rename from lib/widgets/grouped_transaction_list/pending_group_header.dart rename to lib/widgets/grouped_transaction_list/default_pending_group_header.dart index 4e45abd..c7b98bb 100644 --- a/lib/widgets/grouped_transaction_list/pending_group_header.dart +++ b/lib/widgets/grouped_transaction_list/default_pending_group_header.dart @@ -6,10 +6,10 @@ import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:moment_dart/moment_dart.dart"; -class PendingGroupHeader extends StatelessWidget { +class DefaultPendingGroupHeader extends StatelessWidget { final Map>? futureTransactions; - const PendingGroupHeader({ + const DefaultPendingGroupHeader({ super.key, required this.futureTransactions, }); diff --git a/lib/widgets/theme_petal_selector.dart b/lib/widgets/theme_petal_selector.dart index 6c988a9..273aace 100644 --- a/lib/widgets/theme_petal_selector.dart +++ b/lib/widgets/theme_petal_selector.dart @@ -93,7 +93,7 @@ class _ThemePetalSelectorState extends State @override Widget build(BuildContext context) { final String currentTheme = LocalPreferences().getCurrentTheme(); - final bool isDark = isThemeDark(currentTheme); + final bool isDark = getTheme(currentTheme).isDark; final int selectedIndex = isDark ? lightDarkThemeMapping.values.toList().indexOf(currentTheme) : lightDarkThemeMapping.keys.toList().indexOf(currentTheme); From 2176173cb5efc192d2bd27f1202de2c0f6da1170 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sat, 23 Nov 2024 18:12:50 +0800 Subject: [PATCH 05/19] enhance pending transactions --- assets/l10n/en_IN.json | 17 ++++---- assets/l10n/en_US.json | 17 ++++---- assets/l10n/it_IT.json | 17 ++++---- assets/l10n/mn_MN.json | 17 ++++---- lib/data/transactions_filter.dart | 34 +++++++-------- lib/data/upcoming_transactions.dart | 35 ++++++---------- lib/objectbox/actions.dart | 4 +- lib/prefs.dart | 2 +- lib/routes.dart | 13 +----- lib/routes/account_page.dart | 2 +- lib/routes/category_page.dart | 2 +- lib/routes/home/home_tab.dart | 23 +++++++++-- .../new_transaction/input_amount_sheet.dart | 2 +- lib/routes/transactions_page.dart | 2 +- .../home/transactions_date_header.dart | 41 +++++++++++-------- lib/widgets/numpad.dart | 2 - 16 files changed, 111 insertions(+), 119 deletions(-) diff --git a/assets/l10n/en_IN.json b/assets/l10n/en_IN.json index 12a334c..2c75d8a 100644 --- a/assets/l10n/en_IN.json +++ b/assets/l10n/en_IN.json @@ -116,7 +116,7 @@ "transaction.pending.preapproved": "Preapproved", "transactions.all": "All transactions", - "transactions.upcoming": "Upcoming transactions", + "transactions.pending": "Pending transactions", "transactions.query.noResult": "No transactions to show", "transactions.query.noResult.description": "Try updating the filters", "transactions.query.clearAll": "Clear filters", @@ -204,8 +204,8 @@ "tabs.home.noTransactions": "No transactions matching the criteria", "tabs.home.noTransactions.addSome": "Click on (+) button below to add a new transaction", "tabs.home.noTransactions.tryChangingFilters": "Try changing the filters", - "tabs.home.upcomingTransactions": "Upcoming ({count})", - "tabs.home.upcomingTransactions.seeAll": "See all", + "tabs.home.pendingTransactions": "Pending ({count})", + "tabs.home.pendingTransactions.seeAll": "See all", "tabs.home.transactionsCount": "{count} transactions", "tabs.home.last7days": "Last 7 days", "tabs.home.totalBalance": "Total balance", @@ -308,14 +308,11 @@ "enum.TransactionType@income": "Income", "enum.TransactionType@expense": "Expense", "enum.TransactionType@transfer": "Transfer", - "enum.UpcomingTransactionsDuration@none": "None", + "enum.UpcomingTransactionsDuration@next1Days": "Next 1 day", + "enum.UpcomingTransactionsDuration@next2Days": "Next 2 days", + "enum.UpcomingTransactionsDuration@next3Days": "Next 3 days", + "enum.UpcomingTransactionsDuration@next5Days": "Next 5 days", "enum.UpcomingTransactionsDuration@next7Days": "Next 7 days", - "enum.UpcomingTransactionsDuration@next14Days": "Next 14 days", - "enum.UpcomingTransactionsDuration@next30Days": "Next 30 days", - "enum.UpcomingTransactionsDuration@thisWeek": "This week", - "enum.UpcomingTransactionsDuration@thisMonth": "This month", - "enum.UpcomingTransactionsDuration@thisYear": "This year", - "enum.UpcomingTransactionsDuration@allTime": "All-time", "enum.CSVHeadersV1": "CSV Headers", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index 5d67204..6198602 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -116,7 +116,7 @@ "transaction.pending.preapproved": "Preapproved", "transactions.all": "All transactions", - "transactions.upcoming": "Upcoming transactions", + "transactions.pending": "Pending transactions", "transactions.query.noResult": "No transactions to show", "transactions.query.noResult.description": "Try updating the filters", "transactions.query.clearAll": "Clear filters", @@ -204,8 +204,8 @@ "tabs.home.noTransactions": "No transactions matching the criteria", "tabs.home.noTransactions.addSome": "Click on (+) button below to add a new transaction", "tabs.home.noTransactions.tryChangingFilters": "Try changing the filters", - "tabs.home.upcomingTransactions": "Upcoming ({count})", - "tabs.home.upcomingTransactions.seeAll": "See all", + "tabs.home.pendingTransactions": "Pending ({count})", + "tabs.home.pendingTransactions.seeAll": "See all", "tabs.home.transactionsCount": "{count} transactions", "tabs.home.last7days": "Last 7 days", "tabs.home.totalBalance": "Total balance", @@ -308,14 +308,11 @@ "enum.TransactionType@income": "Income", "enum.TransactionType@expense": "Expense", "enum.TransactionType@transfer": "Transfer", - "enum.UpcomingTransactionsDuration@none": "None", + "enum.UpcomingTransactionsDuration@next1Days": "Next 1 day", + "enum.UpcomingTransactionsDuration@next2Days": "Next 2 days", + "enum.UpcomingTransactionsDuration@next3Days": "Next 3 days", + "enum.UpcomingTransactionsDuration@next5Days": "Next 5 days", "enum.UpcomingTransactionsDuration@next7Days": "Next 7 days", - "enum.UpcomingTransactionsDuration@next14Days": "Next 14 days", - "enum.UpcomingTransactionsDuration@next30Days": "Next 30 days", - "enum.UpcomingTransactionsDuration@thisWeek": "This week", - "enum.UpcomingTransactionsDuration@thisMonth": "This month", - "enum.UpcomingTransactionsDuration@thisYear": "This year", - "enum.UpcomingTransactionsDuration@allTime": "All-time", "enum.CSVHeadersV1": "CSV Headers", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index 50c9427..26f4541 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -116,7 +116,7 @@ "transaction.pending.preapproved": "Preapprovata", "transactions.all": "Tutte le transazioni", - "transactions.upcoming": "Prossime transazioni", + "transactions.pending": "Transazioni in sospeso", "transactions.query.noResult": "Nessuna transazione da mostrare", "transactions.query.noResult.description": "Prova ad aggiornare i filtri", "transactions.query.clearAll": "Cancella filtri", @@ -204,8 +204,8 @@ "tabs.home.noTransactions": "Nessuna transazione corrisponde ai criteri di ricerca", "tabs.home.noTransactions.addSome": "Clicca sul pulsante (+) qui sotto per aggiungere una nuova transazione", "tabs.home.noTransactions.tryChangingFilters": "Prova a modificare i filtri", - "tabs.home.upcomingTransactions": "Prossimi ({count})", - "tabs.home.upcomingTransactions.seeAll": "Vedi tutto", + "tabs.home.pendingTransactions": "In attesa ({count})", + "tabs.home.pendingTransactions.seeAll": "Vedi tutto", "tabs.home.transactionsCount": "{count} transazioni", "tabs.home.last7days": "Ultimi 7 giorni", "tabs.home.totalBalance": "Saldo totale", @@ -308,14 +308,11 @@ "enum.TransactionType@income": "Entrata", "enum.TransactionType@expense": "Uscita", "enum.TransactionType@transfer": "Trasferimento", - "enum.UpcomingTransactionsDuration@none": "Nessuno", + "enum.UpcomingTransactionsDuration@next1Days": "Prossimo giorno", + "enum.UpcomingTransactionsDuration@next2Days": "Prossimi 2 giorni", + "enum.UpcomingTransactionsDuration@next3Days": "Prossimi 3 giorni", + "enum.UpcomingTransactionsDuration@next5Days": "Prossimi 5 giorni", "enum.UpcomingTransactionsDuration@next7Days": "Prossimi 7 giorni", - "enum.UpcomingTransactionsDuration@next14Days": "Prossimi 14 giorni", - "enum.UpcomingTransactionsDuration@next30Days": "Prossimi 30 giorni", - "enum.UpcomingTransactionsDuration@thisWeek": "Questa settimana", - "enum.UpcomingTransactionsDuration@thisMonth": "Questo mese", - "enum.UpcomingTransactionsDuration@thisYear": "Quest'anno", - "enum.UpcomingTransactionsDuration@allTime": "Tutte le volte", "enum.CSVHeadersV1": "Intestazioni CSV", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index fb78a93..563e4ff 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -116,7 +116,7 @@ "transaction.pending.preapproved": "Урьдчилан баталгаажуулсан", "transactions.all": "Бүх гүйлгээнүүд", - "transactions.upcoming": "Төлөвлөсөн гүйлгээнүүд", + "transactions.pending": "Хүлээгдэж буй гүйлгээнүүд", "transactions.query.noResult": "Тохирох гүйлгээнүүд олдсонгүй", "transactions.query.noResult.description": "Шүүлтүүрээ өөрчлөөд дахин оролдоно уу", "transactions.query.clearAll": "Шүүлтүүрийг цэвэрлэх", @@ -204,8 +204,8 @@ "tabs.home.noTransactions": "Шүүлтүүрт тохирох гүйлгээ олдсонгүй", "tabs.home.noTransactions.addSome": "Доор байрлах (+) товч дээр дарж гүйлгээ нэмээрэй", "tabs.home.noTransactions.tryChangingFilters": "Шүүлтүүрийг өөрчлөөд үзээрэй", - "tabs.home.upcomingTransactions": "Төлөвлөсөн ({count})", - "tabs.home.upcomingTransactions.seeAll": "Бүгд", + "tabs.home.pendingTransactions": "Төлөвлөсөн ({count})", + "tabs.home.pendingTransactions.seeAll": "Бүгд", "tabs.home.transactionsCount": "{count} гүйлгээ", "tabs.home.last7days": "Сүүлийн 7 хоног", "tabs.home.totalBalance": "Нийт үлдэгдэл", @@ -308,14 +308,11 @@ "enum.TransactionType@income": "Орлого", "enum.TransactionType@expense": "Зарлага", "enum.TransactionType@transfer": "Шилжүүлэг", - "enum.UpcomingTransactionsDuration@none": "Харуулахгүй", + "enum.UpcomingTransactionsDuration@next1Days": "Ирэх 1 хоног", + "enum.UpcomingTransactionsDuration@next2Days": "Ирэх 2 хоног", + "enum.UpcomingTransactionsDuration@next3Days": "Ирэх 3 хоног", + "enum.UpcomingTransactionsDuration@next5Days": "Ирэх 5 хоног", "enum.UpcomingTransactionsDuration@next7Days": "Ирэх 7 хоног", - "enum.UpcomingTransactionsDuration@next14Days": "Ирэх 14 хоног", - "enum.UpcomingTransactionsDuration@next30Days": "Ирэх 30 хоног", - "enum.UpcomingTransactionsDuration@thisWeek": "Энэ долоо хоног", - "enum.UpcomingTransactionsDuration@thisMonth": "Энэ сар", - "enum.UpcomingTransactionsDuration@thisYear": "Энэ жил", - "enum.UpcomingTransactionsDuration@allTime": "Бүгдийг харуулах", "enum.CSVHeadersV1": "CSV Толгой", "enum.CSVHeadersV1@uuid": "Код", diff --git a/lib/data/transactions_filter.dart b/lib/data/transactions_filter.dart index 43eb70b..228a565 100644 --- a/lib/data/transactions_filter.dart +++ b/lib/data/transactions_filter.dart @@ -48,7 +48,7 @@ class TransactionFilter { this.accounts, this.range, this.types, - this.isPending = false, + this.isPending, this.minAmount, this.maxAmount, this.currencies, @@ -186,6 +186,10 @@ class TransactionFilter { Optional>? accounts, bool? sortDescending, TransactionSortField? sortBy, + Optional? isPending, + Optional? minAmount, + Optional? maxAmount, + Optional>? currencies, }) { return TransactionFilter( types: types == null ? this.types : types.value, @@ -195,25 +199,13 @@ class TransactionFilter { accounts: accounts == null ? this.accounts : accounts.value, sortBy: sortBy ?? this.sortBy, sortDescending: sortDescending ?? this.sortDescending, + isPending: isPending == null ? this.isPending : isPending.value, + minAmount: minAmount == null ? this.minAmount : minAmount.value, + maxAmount: maxAmount == null ? this.maxAmount : maxAmount.value, + currencies: currencies == null ? this.currencies : currencies.value, ); } - /// Returns a filter with planned transactions - /// - /// Overrides [to] of the TimeRange - TransactionFilter withPlannedTransactions(int days) => copyWithOptional( - range: range == null - ? null - : Optional( - CustomTimeRange( - range!.from, - Moment.startOfTomorrow() - .add(Duration(days: days - 1)) - .endOfDay(), - ), - ), - ); - @override int get hashCode => Object.hashAll([ types, @@ -223,6 +215,10 @@ class TransactionFilter { accounts, sortDescending, sortBy, + isPending, + minAmount, + maxAmount, + currencies, ]); @override @@ -237,6 +233,10 @@ class TransactionFilter { other.sortDescending == sortDescending && other.sortBy == sortBy && other.searchData == searchData && + other.isPending == isPending && + other.minAmount == minAmount && + other.maxAmount == maxAmount && + setEquals(other.currencies?.toSet(), currencies?.toSet()) && setEquals(other.types?.toSet(), types?.toSet()) && setEquals(other.categories?.toSet(), categories?.toSet()) && setEquals(other.accounts?.toSet(), accounts?.toSet()); diff --git a/lib/data/upcoming_transactions.dart b/lib/data/upcoming_transactions.dart index c3b3ade..a1fcf67 100644 --- a/lib/data/upcoming_transactions.dart +++ b/lib/data/upcoming_transactions.dart @@ -5,14 +5,11 @@ import "package:moment_dart/moment_dart.dart"; @JsonEnum(valueField: "value") enum UpcomingTransactionsDuration implements LocalizedEnum { - none("none"), - next7Days("next7Days"), - next14Days("next14Days"), - next30Days("next30Days"), - thisWeek("thisWeek"), - thisMonth("thisMonth"), - thisYear("thisYear"), - allTime("allTime"); + next1Days("next1Days"), + next2Days("next2Days"), + next3Days("next3Days"), + next5Days("next5Days"), + next7Days("next7Days"); final String value; @@ -26,22 +23,16 @@ enum UpcomingTransactionsDuration implements LocalizedEnum { DateTime? endsAt([DateTime? anchor]) { final Moment now = anchor?.toMoment() ?? Moment.now(); switch (this) { - case UpcomingTransactionsDuration.none: - return null; + case UpcomingTransactionsDuration.next1Days: + return now.add(Duration(days: 1)).endOfDay(); + case UpcomingTransactionsDuration.next2Days: + return now.add(Duration(days: 2)).endOfDay(); + case UpcomingTransactionsDuration.next3Days: + return now.add(Duration(days: 3)).endOfDay(); + case UpcomingTransactionsDuration.next5Days: + return now.add(Duration(days: 5)).endOfDay(); case UpcomingTransactionsDuration.next7Days: return now.add(Duration(days: 7)).endOfDay(); - case UpcomingTransactionsDuration.next14Days: - return now.add(Duration(days: 14)).endOfDay(); - case UpcomingTransactionsDuration.next30Days: - return now.add(Duration(days: 30)).endOfDay(); - case UpcomingTransactionsDuration.thisWeek: - return now.endOfLocalWeek(); - case UpcomingTransactionsDuration.thisMonth: - return now.endOfMonth(); - case UpcomingTransactionsDuration.thisYear: - return now.endOfYear(); - case UpcomingTransactionsDuration.allTime: - return Moment.maxValue; } } diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index 40b325f..787eb27 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -530,8 +530,8 @@ extension TransactionListActions on Iterable { return value; } - List filter(TransactionFilter filter) => - where((Transaction t) => filter.predicates + List filter(List predicates) => + where((Transaction t) => predicates .map((predicate) => predicate(t)) .every((element) => element)).toList(); diff --git a/lib/prefs.dart b/lib/prefs.dart index 19b81b2..4cb8f07 100644 --- a/lib/prefs.dart +++ b/lib/prefs.dart @@ -24,7 +24,7 @@ class LocalPreferences { static const UpcomingTransactionsDuration homeTabPlannedTransactionsDurationDefault = - UpcomingTransactionsDuration.thisWeek; + UpcomingTransactionsDuration.next3Days; /// Main currency used in the app late final PrimitiveSettingsEntry primaryCurrency; diff --git a/lib/routes.dart b/lib/routes.dart index 08f212d..67fe4e2 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -37,7 +37,6 @@ import "package:flow/routes/utils/edit_markdown_page.dart"; import "package:flow/sync/export/mode.dart"; import "package:flow/sync/import/import_v1.dart"; import "package:flow/utils/utils.dart"; -import "package:flow/widgets/general/info_text.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:moment_dart/moment_dart.dart"; @@ -78,17 +77,9 @@ final router = GoRouter( ), ), GoRoute( - path: "/transactions/upcoming", + path: "/transactions/pending", builder: (context, state) => TransactionsPage.upcoming( - title: "transactions.upcoming".t(context), - header: InfoText( - singleLine: true, - child: Text( - "account.balance.upcomingDescription".t(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), + title: "transactions.pending".t(context), ), ), GoRoute( diff --git a/lib/routes/account_page.dart b/lib/routes/account_page.dart index cf0ba1b..c2749c7 100644 --- a/lib/routes/account_page.dart +++ b/lib/routes/account_page.dart @@ -176,7 +176,7 @@ class _AccountPageState extends State { headerBuilder: (pendingGroup, range, rangeTransactions) => TransactionListDateHeader( transactions: rangeTransactions, - date: range.from, + range: range, pendingGroup: pendingGroup, ), ) diff --git a/lib/routes/category_page.dart b/lib/routes/category_page.dart index 1faaa2f..b1af337 100644 --- a/lib/routes/category_page.dart +++ b/lib/routes/category_page.dart @@ -192,7 +192,7 @@ class _CategoryPageState extends State { headerBuilder: (pendingGroup, range, rangeTransactions) => TransactionListDateHeader( transactions: rangeTransactions, - date: range.from, + range: range, pendingGroup: pendingGroup, ), ) diff --git a/lib/routes/home/home_tab.dart b/lib/routes/home/home_tab.dart index bae937c..b804aea 100644 --- a/lib/routes/home/home_tab.dart +++ b/lib/routes/home/home_tab.dart @@ -2,6 +2,7 @@ import "package:flow/data/exchange_rates.dart"; import "package:flow/data/transactions_filter.dart"; import "package:flow/data/upcoming_transactions.dart"; import "package:flow/entity/transaction.dart"; +import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; import "package:flow/services/exchange_rates.dart"; @@ -16,6 +17,8 @@ import "package:flow/widgets/home/transactions_date_header.dart"; import "package:flow/widgets/rates_missing_warning.dart"; import "package:flow/widgets/utils/time_and_range.dart"; import "package:flutter/material.dart"; +import "package:go_router/go_router.dart"; +import "package:material_symbols_icons/symbols.dart"; import "package:moment_dart/moment_dart.dart"; class HomeTab extends StatefulWidget { @@ -94,7 +97,8 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { .queryBuilder() .watch(triggerImmediately: true) .map( - (event) => event.find().search(currentFilter.searchData), + (event) => + event.find().filter(currentFilterWithPlanned.postPredicates), ), builder: (context, snapshot) { final DateTime now = DateTime.now().startOfNextMinute(); @@ -157,7 +161,10 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { .where((transaction) => transaction.transactionDate.isAfter(now) || transaction.isPending == true) - .groupByDate(); + .groupByRange( + rangeFn: (transaction) => + LocalWeekTimeRange(transaction.transactionDate), + ); final bool shouldCombineTransferIfNeeded = currentFilter.accounts?.isNotEmpty != true; @@ -193,8 +200,16 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { ) => TransactionListDateHeader( transactions: transactions, - date: range.from, - pendingGroup: pendingGroup == true, + range: range, + pendingGroup: pendingGroup, + action: pendingGroup + ? TextButton.icon( + onPressed: () => context.push("/transactions/pending"), + label: Text("tabs.home.pendingTransactions.seeAll".t(context)), + icon: Icon(Symbols.arrow_right_alt_rounded), + iconAlignment: IconAlignment.end, + ) + : null, ), ); } diff --git a/lib/routes/new_transaction/input_amount_sheet.dart b/lib/routes/new_transaction/input_amount_sheet.dart index 560d1d2..dfc6cfd 100644 --- a/lib/routes/new_transaction/input_amount_sheet.dart +++ b/lib/routes/new_transaction/input_amount_sheet.dart @@ -118,7 +118,7 @@ class _InputAmountSheetState extends State scrollableContentMaxHeight: MediaQuery.of(context).size.height * 0.8, topSpacing: 0.0, child: LayoutBuilder(builder: (context, size) { - final double width = min(size.maxWidth, 400.0); + final double width = min(size.maxWidth, 440.0); return SingleChildScrollView( child: ConstrainedBox( diff --git a/lib/routes/transactions_page.dart b/lib/routes/transactions_page.dart index 7b08208..541b2d4 100644 --- a/lib/routes/transactions_page.dart +++ b/lib/routes/transactions_page.dart @@ -123,8 +123,8 @@ class _TransactionsPageState extends State { headerBuilder: (pendingGroup, range, transactions) => TransactionListDateHeader( pendingGroup: pendingGroup, + range: range, transactions: transactions, - date: range.from, ), pendingDivider: WavyDivider(), header: widget.header, diff --git a/lib/widgets/home/transactions_date_header.dart b/lib/widgets/home/transactions_date_header.dart index 516d1c3..c45c01b 100644 --- a/lib/widgets/home/transactions_date_header.dart +++ b/lib/widgets/home/transactions_date_header.dart @@ -8,7 +8,7 @@ import "package:flutter/widgets.dart"; import "package:moment_dart/moment_dart.dart"; class TransactionListDateHeader extends StatefulWidget { - final DateTime date; + final TimeRange range; final List transactions; final Widget? action; @@ -16,17 +16,21 @@ class TransactionListDateHeader extends StatefulWidget { /// Hides count and flow final bool pendingGroup; + final bool resolveNonPrimaryCurrencies; + const TransactionListDateHeader({ super.key, required this.transactions, - required this.date, + required this.range, this.action, this.pendingGroup = false, + this.resolveNonPrimaryCurrencies = true, }); const TransactionListDateHeader.pendingGroup({ super.key, - required this.date, + required this.range, this.action, + this.resolveNonPrimaryCurrencies = true, }) : pendingGroup = true, transactions = const []; @@ -56,14 +60,12 @@ class _TransactionListDateHeaderState extends State { @override Widget build(BuildContext context) { final Widget title = Text( - widget.date.toMoment().calendar(omitHours: true), + widget.pendingGroup + ? "transactions.pending".t(context) + : _getRangeTitle(widget.range), style: context.textTheme.headlineSmall, ); - if (widget.pendingGroup) { - return title; - } - final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); final double flow = widget.transactions @@ -76,20 +78,20 @@ class _TransactionListDateHeaderState extends State { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - title, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + title, + if (!widget.pendingGroup) Text( "${Money(flow, primaryCurrency).formattedCompact}${containsNonPrimaryCurrency ? '+' : ''} • ${'tabs.home.transactionsCount'.t(context, widget.transactions.renderableCount)}", style: context.textTheme.labelMedium, ), - ], - ), + ], ), const SizedBox(width: 16.0), + Spacer(), if (widget.action != null) Flexible( fit: FlexFit.tight, @@ -105,4 +107,11 @@ class _TransactionListDateHeaderState extends State { if (!mounted) return; setState(() {}); } + + String _getRangeTitle(TimeRange range) { + return switch (range) { + DayTimeRange() => range.from.toMoment().calendar(omitHours: true), + _ => range.format(), + }; + } } diff --git a/lib/widgets/numpad.dart b/lib/widgets/numpad.dart index eeb2ec4..efc0983 100644 --- a/lib/widgets/numpad.dart +++ b/lib/widgets/numpad.dart @@ -32,8 +32,6 @@ class Numpad extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO (sadespresso) on phones with wider display, or wider phones, or tablets, come up with a different solution to make a numpad. - final width = this.width ?? MediaQuery.of(context).size.width; final double totalHorizontalPadding = padding.left + From 4fc911c3c44171f135de972adc967e411d218231 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Nov 2024 14:49:50 +0800 Subject: [PATCH 06/19] Beta next rc --- .markdownlint.json | 3 +- CHANGELOG.md | 18 ++++ analysis_options.yaml | 1 + assets/l10n/en_IN.json | 17 ++-- assets/l10n/en_US.json | 17 ++-- assets/l10n/it_IT.json | 17 ++-- assets/l10n/mn_MN.json | 17 ++-- lib/constants.dart | 2 +- lib/data/money.dart | 2 +- ...art => pending_transactions_duration.dart} | 20 ++-- lib/data/transactions_filter/search_data.dart | 2 +- lib/entity/profile.dart | 2 +- .../transaction/extensions/default/geo.dart | 2 +- .../extensions/default/transfer.dart | 2 +- lib/l10n/flow_localizations.dart | 2 +- lib/main.dart | 1 - lib/prefs.dart | 12 +-- lib/routes.dart | 2 +- lib/routes/account_page.dart | 2 +- lib/routes/category_page.dart | 2 +- lib/routes/home/home_tab.dart | 60 ++++++------ .../new_transaction/description_section.dart | 3 +- .../preferences/home_tab_preferences.dart | 8 +- lib/routes/preferences_page.dart | 6 +- lib/routes/transactions_page.dart | 92 ++++++++++--------- lib/utils/extensions.dart | 2 + lib/utils/extensions/transaction.dart | 16 ++++ lib/utils/shortcut.dart | 2 +- lib/widgets/general/money_text.dart | 2 +- lib/widgets/general/wavy_divider.dart | 7 +- lib/widgets/grouped_transaction_list.dart | 21 ++--- .../default_pending_group_header.dart | 48 ---------- .../home/pending_transactions_header.dart | 51 ++++++++++ .../select_multi_category_sheet.dart | 3 - lib/widgets/transaction_list_tile.dart | 22 +++-- .../{home => }/transactions_date_header.dart | 86 +++++++++++------ pubspec.lock | 32 +++---- pubspec.yaml | 8 +- 38 files changed, 341 insertions(+), 271 deletions(-) rename lib/data/{upcoming_transactions.dart => pending_transactions_duration.dart} (64%) create mode 100644 lib/utils/extensions/transaction.dart delete mode 100644 lib/widgets/grouped_transaction_list/default_pending_group_header.dart create mode 100644 lib/widgets/home/home/pending_transactions_header.dart rename lib/widgets/{home => }/transactions_date_header.dart (51%) diff --git a/.markdownlint.json b/.markdownlint.json index 3348608..2f00790 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,5 +1,6 @@ { "MD013": { "line_length": 120 - } + }, + "MD024": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4954249..ca67f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## Beta next + +### New features + +* Title flow now converts all currencies into primary + +### Changes + +* Renamed upcoming/planned -> Pending transactions +* Home tab pending transactions time range settings has been revised +* Deprecated old theme system (light/dark). If you missed an update since + this features was introduced, you will need to set up your themes agian. + +### Fixes and enhancements + +* Transaction list tile title color is now fixed in light themes +* Wavy divider color now follows the theme change + ## Beta 0.8.1 ### New features diff --git a/analysis_options.yaml b/analysis_options.yaml index fd1110a..2cd3d7c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,7 @@ linter: rules: unawaited_futures: true prefer_double_quotes: true + type_annotate_public_apis: true analyzer: exclude: - "example/**" diff --git a/assets/l10n/en_IN.json b/assets/l10n/en_IN.json index 2c75d8a..8a17ebe 100644 --- a/assets/l10n/en_IN.json +++ b/assets/l10n/en_IN.json @@ -177,10 +177,8 @@ "preferences.transfer.excludeTransferFromFlow": "Exclude from totals", "preferences.transfer.excludeTransferFromFlow.description": "Don't count towards total expense/income", "preferences.home": "Home page", - "preferences.home.upcoming": "Upcoming transactions", - "preferences.home.upcoming.description": "Shows planned transactions for the selected duration", - "preferences.home.upcoming.alwaysVisible": "Always show", - "preferences.home.upcoming.alwaysVisible.description": "Stay visible when the filters are active", + "preferences.home.pending": "Planned transactions", + "preferences.home.pending.description": "Shows upcoming transactions for the selected duration", "preferences.transactionGeo": "Transaction location", "preferences.transactionGeo.enable": "Enable", "preferences.transactionGeo.disableInstructions": "You can hide this section in settings", @@ -205,6 +203,7 @@ "tabs.home.noTransactions.addSome": "Click on (+) button below to add a new transaction", "tabs.home.noTransactions.tryChangingFilters": "Try changing the filters", "tabs.home.pendingTransactions": "Pending ({count})", + "tabs.home.pendingTransactions.needAttention": "{} transactions require confirmation", "tabs.home.pendingTransactions.seeAll": "See all", "tabs.home.transactionsCount": "{count} transactions", "tabs.home.last7days": "Last 7 days", @@ -308,11 +307,11 @@ "enum.TransactionType@income": "Income", "enum.TransactionType@expense": "Expense", "enum.TransactionType@transfer": "Transfer", - "enum.UpcomingTransactionsDuration@next1Days": "Next 1 day", - "enum.UpcomingTransactionsDuration@next2Days": "Next 2 days", - "enum.UpcomingTransactionsDuration@next3Days": "Next 3 days", - "enum.UpcomingTransactionsDuration@next5Days": "Next 5 days", - "enum.UpcomingTransactionsDuration@next7Days": "Next 7 days", + "enum.PendingTransactionsDuration@next1Days": "Next 1 day", + "enum.PendingTransactionsDuration@next2Days": "Next 2 days", + "enum.PendingTransactionsDuration@next3Days": "Next 3 days", + "enum.PendingTransactionsDuration@next5Days": "Next 5 days", + "enum.PendingTransactionsDuration@next7Days": "Next 7 days", "enum.CSVHeadersV1": "CSV Headers", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index 6198602..a4f48c4 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -177,10 +177,8 @@ "preferences.transfer.excludeTransferFromFlow": "Exclude from totals", "preferences.transfer.excludeTransferFromFlow.description": "Don't count towards total expense/income", "preferences.home": "Home page", - "preferences.home.upcoming": "Upcoming transactions", - "preferences.home.upcoming.description": "Shows planned transactions for the selected duration", - "preferences.home.upcoming.alwaysVisible": "Always show", - "preferences.home.upcoming.alwaysVisible.description": "Stay visible when the filters are active", + "preferences.home.pending": "Pending transactions", + "preferences.home.pending.description": "Shows upcoming transactions for the selected duration", "preferences.transactionGeo": "Transaction location", "preferences.transactionGeo.enable": "Enable", "preferences.transactionGeo.disableInstructions": "You can hide this section in settings", @@ -205,6 +203,7 @@ "tabs.home.noTransactions.addSome": "Click on (+) button below to add a new transaction", "tabs.home.noTransactions.tryChangingFilters": "Try changing the filters", "tabs.home.pendingTransactions": "Pending ({count})", + "tabs.home.pendingTransactions.needAttention": "{} transactions require confirmation", "tabs.home.pendingTransactions.seeAll": "See all", "tabs.home.transactionsCount": "{count} transactions", "tabs.home.last7days": "Last 7 days", @@ -308,11 +307,11 @@ "enum.TransactionType@income": "Income", "enum.TransactionType@expense": "Expense", "enum.TransactionType@transfer": "Transfer", - "enum.UpcomingTransactionsDuration@next1Days": "Next 1 day", - "enum.UpcomingTransactionsDuration@next2Days": "Next 2 days", - "enum.UpcomingTransactionsDuration@next3Days": "Next 3 days", - "enum.UpcomingTransactionsDuration@next5Days": "Next 5 days", - "enum.UpcomingTransactionsDuration@next7Days": "Next 7 days", + "enum.PendingTransactionsDuration@next1Days": "Next 1 day", + "enum.PendingTransactionsDuration@next2Days": "Next 2 days", + "enum.PendingTransactionsDuration@next3Days": "Next 3 days", + "enum.PendingTransactionsDuration@next5Days": "Next 5 days", + "enum.PendingTransactionsDuration@next7Days": "Next 7 days", "enum.CSVHeadersV1": "CSV Headers", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index 26f4541..c695193 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -177,10 +177,8 @@ "preferences.transfer.excludeTransferFromFlow": "Escludi dai totali", "preferences.transfer.excludeTransferFromFlow.description": "Non conteggiare verso la spesa/entrata totale", "preferences.home": "Home page", - "preferences.home.upcoming": "Prossime transazioni", - "preferences.home.upcoming.description": "Mostra le transazioni pianificate per la durata selezionata", - "preferences.home.upcoming.alwaysVisible": "Mostra sempre", - "preferences.home.upcoming.alwaysVisible.description": "Rimane visibile quando i filtri sono attivi", + "preferences.home.pending": "Transazioni in sospeso", + "preferences.home.pending.description": "Mostra le transazioni future per il periodo selezionato", "preferences.transactionGeo": "Posizione transazione", "preferences.transactionGeo.enable": "Abilita", "preferences.transactionGeo.disableInstructions": "Puoi nascondere questa sezione nelle impostazioni.", @@ -205,6 +203,7 @@ "tabs.home.noTransactions.addSome": "Clicca sul pulsante (+) qui sotto per aggiungere una nuova transazione", "tabs.home.noTransactions.tryChangingFilters": "Prova a modificare i filtri", "tabs.home.pendingTransactions": "In attesa ({count})", + "tabs.home.pendingTransactions.needAttention": "{} transazioni richiedono conferma", "tabs.home.pendingTransactions.seeAll": "Vedi tutto", "tabs.home.transactionsCount": "{count} transazioni", "tabs.home.last7days": "Ultimi 7 giorni", @@ -308,11 +307,11 @@ "enum.TransactionType@income": "Entrata", "enum.TransactionType@expense": "Uscita", "enum.TransactionType@transfer": "Trasferimento", - "enum.UpcomingTransactionsDuration@next1Days": "Prossimo giorno", - "enum.UpcomingTransactionsDuration@next2Days": "Prossimi 2 giorni", - "enum.UpcomingTransactionsDuration@next3Days": "Prossimi 3 giorni", - "enum.UpcomingTransactionsDuration@next5Days": "Prossimi 5 giorni", - "enum.UpcomingTransactionsDuration@next7Days": "Prossimi 7 giorni", + "enum.PendingTransactionsDuration@next1Days": "Prossimo giorno", + "enum.PendingTransactionsDuration@next2Days": "Prossimi 2 giorni", + "enum.PendingTransactionsDuration@next3Days": "Prossimi 3 giorni", + "enum.PendingTransactionsDuration@next5Days": "Prossimi 5 giorni", + "enum.PendingTransactionsDuration@next7Days": "Prossimi 7 giorni", "enum.CSVHeadersV1": "Intestazioni CSV", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index 563e4ff..86235ec 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -177,10 +177,8 @@ "preferences.transfer.excludeTransferFromFlow": "Нийт дүнд оруулахгүй", "preferences.transfer.excludeTransferFromFlow.description": "Идэвхтэй үед орлого/зарлага-д тоолохгүй", "preferences.home": "Нүүр хуудас", - "preferences.home.upcoming": "Төлөвлөсөн гүйлгээнүүд", - "preferences.home.upcoming.description": "Сонгосон хугацааны төлөвлөгөөт гүйлгээнүүдийг харуулна", - "preferences.home.upcoming.alwaysVisible": "Үргэлж харуулах", - "preferences.home.upcoming.alwaysVisible.description": "Шүүлтүүр өөрчлөгдсөн ч харуулах", + "preferences.home.pending": "Хүлээгдэж буй гүйлгээнүүд", + "preferences.home.pending.description": "Сонгосон хугацаа дахь гүйлгээнүүдийг харуулна", "preferences.transactionGeo": "Гүйлгээний байршил", "preferences.transactionGeo.enable": "Идэвхжүүлэх", "preferences.transactionGeo.disableInstructions": "Энэ хэсгийг тохиргооноос нуух боломжтой", @@ -205,6 +203,7 @@ "tabs.home.noTransactions.addSome": "Доор байрлах (+) товч дээр дарж гүйлгээ нэмээрэй", "tabs.home.noTransactions.tryChangingFilters": "Шүүлтүүрийг өөрчлөөд үзээрэй", "tabs.home.pendingTransactions": "Төлөвлөсөн ({count})", + "tabs.home.pendingTransactions.needAttention": "Баталгаажуулах шаардлагатай {} гүйлгээ байна", "tabs.home.pendingTransactions.seeAll": "Бүгд", "tabs.home.transactionsCount": "{count} гүйлгээ", "tabs.home.last7days": "Сүүлийн 7 хоног", @@ -308,11 +307,11 @@ "enum.TransactionType@income": "Орлого", "enum.TransactionType@expense": "Зарлага", "enum.TransactionType@transfer": "Шилжүүлэг", - "enum.UpcomingTransactionsDuration@next1Days": "Ирэх 1 хоног", - "enum.UpcomingTransactionsDuration@next2Days": "Ирэх 2 хоног", - "enum.UpcomingTransactionsDuration@next3Days": "Ирэх 3 хоног", - "enum.UpcomingTransactionsDuration@next5Days": "Ирэх 5 хоног", - "enum.UpcomingTransactionsDuration@next7Days": "Ирэх 7 хоног", + "enum.PendingTransactionsDuration@next1Days": "Ирэх 1 хоног", + "enum.PendingTransactionsDuration@next2Days": "Ирэх 2 хоног", + "enum.PendingTransactionsDuration@next3Days": "Ирэх 3 хоног", + "enum.PendingTransactionsDuration@next5Days": "Ирэх 5 хоног", + "enum.PendingTransactionsDuration@next7Days": "Ирэх 7 хоног", "enum.CSVHeadersV1": "CSV Толгой", "enum.CSVHeadersV1@uuid": "Код", diff --git a/lib/constants.dart b/lib/constants.dart index dd454ad..92b14b4 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -2,7 +2,7 @@ import "package:flutter/foundation.dart"; import "package:latlong2/latlong.dart"; String appVersion = "0.0.0"; -const debugBuild = false; +const bool debugBuild = false; bool get flowDebugMode => kDebugMode || debugBuild; diff --git a/lib/data/money.dart b/lib/data/money.dart index dc7e976..eec2e8f 100644 --- a/lib/data/money.dart +++ b/lib/data/money.dart @@ -136,7 +136,7 @@ class Money { int get hashCode => Object.hashAll([amount, currency]); @override - toString() { + String toString() { return "Money($currency $amount)"; } diff --git a/lib/data/upcoming_transactions.dart b/lib/data/pending_transactions_duration.dart similarity index 64% rename from lib/data/upcoming_transactions.dart rename to lib/data/pending_transactions_duration.dart index a1fcf67..64cc406 100644 --- a/lib/data/upcoming_transactions.dart +++ b/lib/data/pending_transactions_duration.dart @@ -4,7 +4,7 @@ import "package:json_annotation/json_annotation.dart"; import "package:moment_dart/moment_dart.dart"; @JsonEnum(valueField: "value") -enum UpcomingTransactionsDuration implements LocalizedEnum { +enum PendingTransactionsDuration implements LocalizedEnum { next1Days("next1Days"), next2Days("next2Days"), next3Days("next3Days"), @@ -13,31 +13,31 @@ enum UpcomingTransactionsDuration implements LocalizedEnum { final String value; - const UpcomingTransactionsDuration(this.value); + const PendingTransactionsDuration(this.value); @override String get localizationEnumValue => name; @override - String get localizationEnumName => "UpcomingTransactionsDuration"; + String get localizationEnumName => "PendingTransactionsDuration"; DateTime? endsAt([DateTime? anchor]) { final Moment now = anchor?.toMoment() ?? Moment.now(); switch (this) { - case UpcomingTransactionsDuration.next1Days: + case PendingTransactionsDuration.next1Days: return now.add(Duration(days: 1)).endOfDay(); - case UpcomingTransactionsDuration.next2Days: + case PendingTransactionsDuration.next2Days: return now.add(Duration(days: 2)).endOfDay(); - case UpcomingTransactionsDuration.next3Days: + case PendingTransactionsDuration.next3Days: return now.add(Duration(days: 3)).endOfDay(); - case UpcomingTransactionsDuration.next5Days: + case PendingTransactionsDuration.next5Days: return now.add(Duration(days: 5)).endOfDay(); - case UpcomingTransactionsDuration.next7Days: + case PendingTransactionsDuration.next7Days: return now.add(Duration(days: 7)).endOfDay(); } } - static UpcomingTransactionsDuration? fromJson(Map json) { - return UpcomingTransactionsDuration.values + static PendingTransactionsDuration? fromJson(Map json) { + return PendingTransactionsDuration.values .firstWhereOrNull((element) => element.value == json["value"]); } diff --git a/lib/data/transactions_filter/search_data.dart b/lib/data/transactions_filter/search_data.dart index 5800d13..36753a7 100644 --- a/lib/data/transactions_filter/search_data.dart +++ b/lib/data/transactions_filter/search_data.dart @@ -108,7 +108,7 @@ class TransactionSearchData { [keyword, smartMatch, caseInsensitive, smartMatchThreshold]); @override - operator ==(Object other) { + bool operator ==(Object other) { if (identical(this, other)) { return true; } diff --git a/lib/entity/profile.dart b/lib/entity/profile.dart index d6464c1..ea6b336 100644 --- a/lib/entity/profile.dart +++ b/lib/entity/profile.dart @@ -41,7 +41,7 @@ class Profile implements EntityBase { _$ProfileFromJson(json); Map toJson() => _$ProfileToJson(this); - static createDefaultProfile() { + static void createDefaultProfile() { ObjectBox().box().put( Profile( name: "Default Profile", diff --git a/lib/entity/transaction/extensions/default/geo.dart b/lib/entity/transaction/extensions/default/geo.dart index d8f0e30..399af23 100644 --- a/lib/entity/transaction/extensions/default/geo.dart +++ b/lib/entity/transaction/extensions/default/geo.dart @@ -23,7 +23,7 @@ class Geo extends TransactionExtension implements Jasonable { String? relatedTransactionUuid; @override - setRelatedTransactionUuid(String uuid) => relatedTransactionUuid = uuid; + void setRelatedTransactionUuid(String uuid) => relatedTransactionUuid = uuid; final double? latitude; final double? longitude; diff --git a/lib/entity/transaction/extensions/default/transfer.dart b/lib/entity/transaction/extensions/default/transfer.dart index 186b189..bdbbcac 100644 --- a/lib/entity/transaction/extensions/default/transfer.dart +++ b/lib/entity/transaction/extensions/default/transfer.dart @@ -19,7 +19,7 @@ class Transfer extends TransactionExtension implements Jasonable { String? relatedTransactionUuid; @override - setRelatedTransactionUuid(String uuid) => relatedTransactionUuid = uuid; + void setRelatedTransactionUuid(String uuid) => relatedTransactionUuid = uuid; Transfer({ required super.uuid, diff --git a/lib/l10n/flow_localizations.dart b/lib/l10n/flow_localizations.dart index 12219d6..efa6710 100644 --- a/lib/l10n/flow_localizations.dart +++ b/lib/l10n/flow_localizations.dart @@ -77,7 +77,7 @@ class FlowLocalizations { static int supportedLanguagesCount = supportedLanguages.length; - static printMissingKeys() async { + static void printMissingKeys() async { final Map> languages = {}; for (Locale locale in supportedLanguages) { String value = diff --git a/lib/main.dart b/lib/main.dart index af196f8..5c9da3a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -146,7 +146,6 @@ class FlowState extends State { locale: _locale, routerConfig: router, theme: _themeFactory.materialTheme, - darkTheme: _themeFactory.materialTheme, themeMode: _themeMode, debugShowCheckedModeBanner: false, ); diff --git a/lib/prefs.dart b/lib/prefs.dart index 4cb8f07..5dfcf3e 100644 --- a/lib/prefs.dart +++ b/lib/prefs.dart @@ -4,7 +4,7 @@ import "dart:developer"; import "package:flow/data/exchange_rates_set.dart"; import "package:flow/data/prefs/frecency.dart"; -import "package:flow/data/upcoming_transactions.dart"; +import "package:flow/data/pending_transactions_duration.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/category.dart"; import "package:flow/entity/transaction.dart"; @@ -22,9 +22,9 @@ import "package:shared_preferences/shared_preferences.dart"; class LocalPreferences { final SharedPreferences _prefs; - static const UpcomingTransactionsDuration + static const PendingTransactionsDuration homeTabPlannedTransactionsDurationDefault = - UpcomingTransactionsDuration.next3Days; + PendingTransactionsDuration.next3Days; /// Main currency used in the app late final PrimitiveSettingsEntry primaryCurrency; @@ -53,7 +53,7 @@ class LocalPreferences { late final BoolSettingsEntry excludeTransferFromFlow; /// Shows next [homeTabPlannedTransactionsDays] days of planned transactions in the home tab - late final JsonSettingsEntry + late final JsonSettingsEntry homeTabPlannedTransactionsDuration; late final JsonListSettingsEntry transactionButtonOrder; @@ -112,12 +112,12 @@ class LocalPreferences { initialValue: false, ); homeTabPlannedTransactionsDuration = - JsonSettingsEntry( + JsonSettingsEntry( key: "homeTabPlannedTransactionsDuration", preferences: _prefs, initialValue: homeTabPlannedTransactionsDurationDefault, fromJson: (map) => - UpcomingTransactionsDuration.fromJson(map) ?? + PendingTransactionsDuration.fromJson(map) ?? homeTabPlannedTransactionsDurationDefault, toJson: (data) => data.toJson(), ); diff --git a/lib/routes.dart b/lib/routes.dart index 67fe4e2..a830bb9 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -78,7 +78,7 @@ final router = GoRouter( ), GoRoute( path: "/transactions/pending", - builder: (context, state) => TransactionsPage.upcoming( + builder: (context, state) => TransactionsPage.pending( title: "transactions.pending".t(context), ), ), diff --git a/lib/routes/account_page.dart b/lib/routes/account_page.dart index c2749c7..b9c7039 100644 --- a/lib/routes/account_page.dart +++ b/lib/routes/account_page.dart @@ -12,7 +12,7 @@ import "package:flow/widgets/category/transactions_info.dart"; import "package:flow/widgets/flow_card.dart"; import "package:flow/widgets/general/spinner.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; -import "package:flow/widgets/home/transactions_date_header.dart"; +import "package:flow/widgets/transactions_date_header.dart"; import "package:flow/widgets/no_result.dart"; import "package:flow/widgets/time_range_selector.dart"; import "package:flutter/material.dart"; diff --git a/lib/routes/category_page.dart b/lib/routes/category_page.dart index b1af337..1be9685 100644 --- a/lib/routes/category_page.dart +++ b/lib/routes/category_page.dart @@ -14,7 +14,7 @@ import "package:flow/widgets/category/transactions_info.dart"; import "package:flow/widgets/flow_card.dart"; import "package:flow/widgets/general/spinner.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; -import "package:flow/widgets/home/transactions_date_header.dart"; +import "package:flow/widgets/transactions_date_header.dart"; import "package:flow/widgets/no_result.dart"; import "package:flow/widgets/rates_missing_warning.dart"; import "package:flow/widgets/time_range_selector.dart"; diff --git a/lib/routes/home/home_tab.dart b/lib/routes/home/home_tab.dart index b804aea..59a53ad 100644 --- a/lib/routes/home/home_tab.dart +++ b/lib/routes/home/home_tab.dart @@ -1,24 +1,22 @@ import "package:flow/data/exchange_rates.dart"; import "package:flow/data/transactions_filter.dart"; -import "package:flow/data/upcoming_transactions.dart"; +import "package:flow/data/pending_transactions_duration.dart"; import "package:flow/entity/transaction.dart"; -import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; import "package:flow/services/exchange_rates.dart"; -import "package:flow/utils/optional.dart"; +import "package:flow/utils/utils.dart"; import "package:flow/widgets/default_transaction_filter_head.dart"; import "package:flow/widgets/general/wavy_divider.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; import "package:flow/widgets/home/greetings_bar.dart"; import "package:flow/widgets/home/home/flow_cards.dart"; import "package:flow/widgets/home/home/no_transactions.dart"; -import "package:flow/widgets/home/transactions_date_header.dart"; +import "package:flow/widgets/home/home/pending_transactions_header.dart"; import "package:flow/widgets/rates_missing_warning.dart"; +import "package:flow/widgets/transactions_date_header.dart"; import "package:flow/widgets/utils/time_and_range.dart"; import "package:flutter/material.dart"; -import "package:go_router/go_router.dart"; -import "package:material_symbols_icons/symbols.dart"; import "package:moment_dart/moment_dart.dart"; class HomeTab extends StatefulWidget { @@ -33,7 +31,7 @@ class HomeTab extends StatefulWidget { class _HomeTabState extends State with AutomaticKeepAliveClientMixin { late final AppLifecycleListener _listener; - UpcomingTransactionsDuration _plannedTransactionsDuration = + PendingTransactionsDuration _plannedTransactionsDuration = LocalPreferences.homeTabPlannedTransactionsDurationDefault; final TransactionFilter defaultFilter = TransactionFilter( @@ -157,14 +155,22 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { !transaction.transactionDate.isAfter(now) && transaction.isPending != true) .groupByDate(); - final Map> pendingTransactions = transactions + + final List pendingTransactions = transactions .where((transaction) => transaction.transactionDate.isAfter(now) || transaction.isPending == true) - .groupByRange( - rangeFn: (transaction) => - LocalWeekTimeRange(transaction.transactionDate), - ); + .toList(); + + final int actionNeededCount = pendingTransactions + .where((transaction) => transaction.confirmable()) + .length; + + final Map> pendingTransactionsGrouped = + pendingTransactions.groupByRange( + rangeFn: (transaction) => + CustomTimeRange(Moment.minValue, Moment.maxValue), + ); final bool shouldCombineTransferIfNeeded = currentFilter.accounts?.isNotEmpty != true; @@ -186,7 +192,7 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { ), controller: widget.scrollController, transactions: grouped, - pendingTransactions: pendingTransactions, + pendingTransactions: pendingTransactionsGrouped, shouldCombineTransferIfNeeded: shouldCombineTransferIfNeeded, pendingDivider: const WavyDivider(), listPadding: const EdgeInsets.only( @@ -197,20 +203,20 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { pendingGroup, range, transactions, - ) => - TransactionListDateHeader( - transactions: transactions, - range: range, - pendingGroup: pendingGroup, - action: pendingGroup - ? TextButton.icon( - onPressed: () => context.push("/transactions/pending"), - label: Text("tabs.home.pendingTransactions.seeAll".t(context)), - icon: Icon(Symbols.arrow_right_alt_rounded), - iconAlignment: IconAlignment.end, - ) - : null, - ), + ) { + if (pendingGroup) { + return PendingTransactionsHeader( + transactions: transactions, + range: range, + badgeCount: actionNeededCount, + ); + } + + return TransactionListDateHeader( + transactions: transactions, + range: range, + ); + }, ); } diff --git a/lib/routes/new_transaction/description_section.dart b/lib/routes/new_transaction/description_section.dart index bff1df4..288d6c1 100644 --- a/lib/routes/new_transaction/description_section.dart +++ b/lib/routes/new_transaction/description_section.dart @@ -158,7 +158,8 @@ class DescriptionSection extends StatelessWidget { controller.text = result; } - void onTapLink(BuildContext context, text, href, title) { + void onTapLink( + BuildContext context, String text, String? href, String title) { log("[Flow] Tapped link: $text, $href, $title"); if (href == null) { diff --git a/lib/routes/preferences/home_tab_preferences.dart b/lib/routes/preferences/home_tab_preferences.dart index 5546068..c51324e 100644 --- a/lib/routes/preferences/home_tab_preferences.dart +++ b/lib/routes/preferences/home_tab_preferences.dart @@ -1,4 +1,4 @@ -import "package:flow/data/upcoming_transactions.dart"; +import "package:flow/data/pending_transactions_duration.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/l10n/named_enum.dart"; import "package:flow/prefs.dart"; @@ -16,7 +16,7 @@ class HomeTabPreferencesPage extends StatefulWidget { class _HomeTabPreferencesPageState extends State { @override Widget build(BuildContext context) { - final UpcomingTransactionsDuration homeTabPlannedTransactionsDuration = + final PendingTransactionsDuration homeTabPlannedTransactionsDuration = LocalPreferences().homeTabPlannedTransactionsDuration.get() ?? LocalPreferences.homeTabPlannedTransactionsDurationDefault; @@ -37,7 +37,7 @@ class _HomeTabPreferencesPageState extends State { child: Wrap( spacing: 12.0, runSpacing: 8.0, - children: UpcomingTransactionsDuration.values + children: PendingTransactionsDuration.values .map( (value) => FilterChip( showCheckmark: false, @@ -71,7 +71,7 @@ class _HomeTabPreferencesPageState extends State { } void updateHomeTabPlannedTransactionsDays( - UpcomingTransactionsDuration duration) async { + PendingTransactionsDuration duration) async { await LocalPreferences().homeTabPlannedTransactionsDuration.set(duration); if (mounted) setState(() {}); diff --git a/lib/routes/preferences_page.dart b/lib/routes/preferences_page.dart index a8f87cd..cfb6f10 100644 --- a/lib/routes/preferences_page.dart +++ b/lib/routes/preferences_page.dart @@ -2,7 +2,7 @@ import "dart:developer"; import "dart:io"; import "package:app_settings/app_settings.dart"; -import "package:flow/data/upcoming_transactions.dart"; +import "package:flow/data/pending_transactions_duration.dart"; import "package:flow/l10n/flow_localizations.dart"; import "package:flow/l10n/named_enum.dart"; import "package:flow/prefs.dart"; @@ -30,7 +30,7 @@ class _PreferencesPageState extends State { final FlowColorScheme currentTheme = getTheme(LocalPreferences().themeName.get()); - final UpcomingTransactionsDuration homeTabPlannedTransactionsDuration = + final PendingTransactionsDuration homeTabPlannedTransactionsDuration = LocalPreferences().homeTabPlannedTransactionsDuration.get() ?? LocalPreferences.homeTabPlannedTransactionsDurationDefault; @@ -48,7 +48,7 @@ class _PreferencesPageState extends State { body: SafeArea( child: ListView(children: [ ListTile( - title: Text("preferences.home.upcoming".t(context)), + title: Text("preferences.home.pending".t(context)), subtitle: Text( homeTabPlannedTransactionsDuration.localizedNameContext(context), ), diff --git a/lib/routes/transactions_page.dart b/lib/routes/transactions_page.dart index 541b2d4..fd6725f 100644 --- a/lib/routes/transactions_page.dart +++ b/lib/routes/transactions_page.dart @@ -5,7 +5,7 @@ import "package:flow/objectbox/objectbox.g.dart"; import "package:flow/widgets/general/spinner.dart"; import "package:flow/widgets/general/wavy_divider.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; -import "package:flow/widgets/home/transactions_date_header.dart"; +import "package:flow/widgets/transactions_date_header.dart"; import "package:flutter/material.dart"; import "package:moment_dart/moment_dart.dart"; @@ -59,18 +59,20 @@ class TransactionsPage extends StatefulWidget { ); } - factory TransactionsPage.upcoming({ + factory TransactionsPage.pending({ Key? key, DateTime? anchor, String? title, Widget? header, }) { - anchor ??= DateTime.now(); + anchor ??= DateTime.now().startOfMinute(); final QueryBuilder queryBuilder = ObjectBox() .box() - .query(Transaction_.transactionDate.greaterThanDate(anchor)) - .order(Transaction_.transactionDate, flags: Order.descending); + .query(Transaction_.transactionDate + .greaterThanDate(anchor) + .or(Transaction_.isPending.equals(true))) + .order(Transaction_.transactionDate); return TransactionsPage( query: queryBuilder, @@ -88,49 +90,49 @@ class _TransactionsPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: widget.title == null ? null : Text(widget.title!), - ), - body: SafeArea( - child: StreamBuilder>( - stream: widget.query - .watch(triggerImmediately: true) - .map((event) => event.find()), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Spinner.center(); - } + appBar: AppBar( + title: widget.title == null ? null : Text(widget.title!), + ), + body: SafeArea( + child: StreamBuilder>( + stream: widget.query + .watch(triggerImmediately: true) + .map((event) => event.find()), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Spinner.center(); + } - final DateTime now = DateTime.now().startOfNextMinute(); + final DateTime now = DateTime.now().startOfNextMinute(); - final Map> transactions = snapshot - .requireData - .where((transaction) => - !transaction.transactionDate.isAfter(now) && - transaction.isPending != true) - .groupByDate(); - final Map> pendingTransactions = - snapshot - .requireData - .where((transaction) => - transaction.transactionDate.isAfter(now) || - transaction.isPending == true) - .groupByDate(); + final Map> transactions = snapshot + .requireData + .where((transaction) => + !transaction.transactionDate.isAfter(now) && + transaction.isPending != true) + .groupByDate(); + final Map> pendingTransactions = + snapshot.requireData + .where((transaction) => + transaction.transactionDate.isAfter(now) || + transaction.isPending == true) + .groupByDate(); - return GroupedTransactionList( + return GroupedTransactionList( + transactions: transactions, + pendingTransactions: pendingTransactions, + headerBuilder: (pendingGroup, range, transactions) => + TransactionListDateHeader( + pendingGroup: pendingGroup, + range: range, transactions: transactions, - pendingTransactions: pendingTransactions, - headerBuilder: (pendingGroup, range, transactions) => - TransactionListDateHeader( - pendingGroup: pendingGroup, - range: range, - transactions: transactions, - ), - pendingDivider: WavyDivider(), - header: widget.header, - ); - }, - ), - )); + ), + pendingDivider: WavyDivider(), + header: widget.header, + ); + }, + ), + ), + ); } } diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart index 443679b..f116f3b 100644 --- a/lib/utils/extensions.dart +++ b/lib/utils/extensions.dart @@ -4,3 +4,5 @@ export "extensions/num.dart"; export "extensions/string.dart"; export "extensions/toast.dart"; export "extensions/custom_dialogs.dart"; +export "extensions/transaction.dart"; +export "extensions/transaction_context_actions.dart"; diff --git a/lib/utils/extensions/transaction.dart b/lib/utils/extensions/transaction.dart new file mode 100644 index 0000000..6c05664 --- /dev/null +++ b/lib/utils/extensions/transaction.dart @@ -0,0 +1,16 @@ +import "package:flow/entity/transaction.dart"; +import "package:moment_dart/moment_dart.dart"; + +extension TransactionHelpers on Transaction { + bool confirmable([DateTime? anchor]) => + isPending == true && + transactionDate.isPastAnchored( + anchor ?? Moment.now().endOfNextMinute(), + ); + + bool holdable([DateTime? anchor]) => + isPending != true && + transactionDate.isFutureAnchored( + anchor ?? Moment.now().startOfMinute(), + ); +} diff --git a/lib/utils/shortcut.dart b/lib/utils/shortcut.dart index 96a8ef4..4eeecd4 100644 --- a/lib/utils/shortcut.dart +++ b/lib/utils/shortcut.dart @@ -11,7 +11,7 @@ bool _shouldUseMeta() => Platform.isMacOS || Platform.isIOS; /// * `Control` for other platforms /// /// It's also possible to pass [shift] and [alt] to the constructor. -osSingleActivator( +SingleActivator osSingleActivator( LogicalKeyboardKey key, [ bool shift = false, bool alt = false, diff --git a/lib/widgets/general/money_text.dart b/lib/widgets/general/money_text.dart index 4627dd0..03bb0aa 100644 --- a/lib/widgets/general/money_text.dart +++ b/lib/widgets/general/money_text.dart @@ -79,7 +79,7 @@ class _MoneyTextState extends State { late bool abbreviate; @override - initState() { + void initState() { super.initState(); abbreviate = widget.initiallyAbbreviated; diff --git a/lib/widgets/general/wavy_divider.dart b/lib/widgets/general/wavy_divider.dart index a2783c3..d0cf3da 100644 --- a/lib/widgets/general/wavy_divider.dart +++ b/lib/widgets/general/wavy_divider.dart @@ -26,19 +26,20 @@ class WavyDivider extends StatelessWidget { @override Widget build(BuildContext context) { + final Color color = this.color ?? Theme.of(context).dividerColor; + return SizedBox( width: double.infinity, height: height, child: ClipRect( child: CustomPaint( + key: ValueKey(color), painter: WavyDividerPainter( - color: color ?? Theme.of(context).dividerColor, + color: color, height: height, waveWidth: waveWidth, strokeWidth: strokeWidth, ), - isComplex: false, - willChange: false, ), ), ); diff --git a/lib/widgets/grouped_transaction_list.dart b/lib/widgets/grouped_transaction_list.dart index 4450967..7877120 100644 --- a/lib/widgets/grouped_transaction_list.dart +++ b/lib/widgets/grouped_transaction_list.dart @@ -2,7 +2,6 @@ import "package:flow/data/transactions_filter.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/prefs.dart"; import "package:flow/utils/extensions/transaction_context_actions.dart"; -import "package:flow/widgets/grouped_transaction_list/default_pending_group_header.dart"; import "package:flow/widgets/transaction_list_tile.dart"; import "package:flutter/material.dart"; import "package:moment_dart/moment_dart.dart"; @@ -33,6 +32,9 @@ class GroupedTransactionList extends StatefulWidget { /// is based on [anchor] final Widget? pendingDivider; + /// A widget rendered after all pending transactions + final Widget? pendingTrailing; + /// Used to determine which transactions are considered future or past. /// /// For now, only [pendingDivider] makes use of this @@ -45,8 +47,6 @@ class GroupedTransactionList extends StatefulWidget { final Widget? header; - final bool implyHeader; - final TransactionFilter? filter; /// Set this to [true] to make it always unobscured @@ -64,10 +64,10 @@ class GroupedTransactionList extends StatefulWidget { this.controller, this.header, this.pendingDivider, + this.pendingTrailing, this.anchor, this.headerPadding, this.filter, - this.implyHeader = true, this.listPadding = const EdgeInsets.symmetric(vertical: 16.0), this.itemPadding = const EdgeInsets.symmetric( horizontal: 16.0, @@ -85,6 +85,8 @@ class GroupedTransactionList extends StatefulWidget { class _GroupedTransactionListState extends State { late bool globalPrivacyMode; + Widget? get header => widget.header; + @override void initState() { super.initState(); @@ -105,20 +107,14 @@ class _GroupedTransactionListState extends State { final bool combineTransfers = widget.shouldCombineTransferIfNeeded && LocalPreferences().combineTransferTransactions.get(); - final Widget? header = widget.header ?? - (widget.implyHeader - ? DefaultPendingGroupHeader( - futureTransactions: widget.pendingTransactions, - ) - : null); - final List flattened = [ - if (header != null) header, + if (header != null) header!, if (widget.pendingTransactions != null) for (final entry in widget.pendingTransactions!.entries) ...[ widget.headerBuilder(true, entry.key, entry.value), ...entry.value, ], + if (widget.pendingTrailing != null) widget.pendingTrailing!, if (widget.pendingDivider != null && widget.pendingTransactions?.isNotEmpty == true && widget.transactions.isNotEmpty) @@ -135,6 +131,7 @@ class _GroupedTransactionListState extends State { controller: widget.controller, padding: widget.listPadding, itemBuilder: (context, index) => switch (flattened[index]) { + (Padding widgetWithPadding) => widgetWithPadding, (Widget header) => Padding( padding: headerPadding.copyWith( top: diff --git a/lib/widgets/grouped_transaction_list/default_pending_group_header.dart b/lib/widgets/grouped_transaction_list/default_pending_group_header.dart deleted file mode 100644 index c7b98bb..0000000 --- a/lib/widgets/grouped_transaction_list/default_pending_group_header.dart +++ /dev/null @@ -1,48 +0,0 @@ -import "package:flow/entity/transaction.dart"; -import "package:flow/l10n/extensions.dart"; -import "package:flow/objectbox/actions.dart"; -import "package:flow/theme/theme.dart"; -import "package:flutter/material.dart"; -import "package:go_router/go_router.dart"; -import "package:moment_dart/moment_dart.dart"; - -class DefaultPendingGroupHeader extends StatelessWidget { - final Map>? futureTransactions; - - const DefaultPendingGroupHeader({ - super.key, - required this.futureTransactions, - }); - - @override - Widget build(BuildContext context) { - if (futureTransactions == null || futureTransactions!.isEmpty) { - return SizedBox.shrink(); - } - - final Map> transactions = futureTransactions!; - - final int count = transactions.values.fold( - 0, - (previousValue, element) => previousValue + element.renderableCount, - ); - - return Row( - children: [ - Expanded( - child: Text( - "tabs.home.upcomingTransactions".t(context, count), - style: context.textTheme.bodyLarge?.semi(context), - ), - ), - const SizedBox(width: 16.0), - TextButton( - onPressed: () => context.push("/transactions/upcoming"), - child: Text( - "tabs.home.upcomingTransactions.seeAll".t(context), - ), - ) - ], - ); - } -} diff --git a/lib/widgets/home/home/pending_transactions_header.dart b/lib/widgets/home/home/pending_transactions_header.dart new file mode 100644 index 0000000..0902b31 --- /dev/null +++ b/lib/widgets/home/home/pending_transactions_header.dart @@ -0,0 +1,51 @@ +import "package:flow/entity/transaction.dart"; +import "package:flow/l10n/extensions.dart"; +import "package:flow/theme/theme.dart"; +import "package:flow/widgets/transactions_date_header.dart"; +import "package:flutter/material.dart"; +import "package:go_router/go_router.dart"; +import "package:material_symbols_icons/symbols.dart"; +import "package:moment_dart/moment_dart.dart"; + +class PendingTransactionsHeader extends StatelessWidget { + final TimeRange range; + final List transactions; + final int? badgeCount; + + const PendingTransactionsHeader({ + super.key, + required this.range, + required this.transactions, + this.badgeCount, + }); + + @override + Widget build(BuildContext context) { + return TransactionListDateHeader( + transactions: transactions, + range: range, + pendingGroup: true, + resolveNonPrimaryCurrencies: false, + titleOverride: Row( + children: [ + Text("transactions.pending".t(context)), + if (badgeCount != null && badgeCount! > 0) + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Badge.count( + count: badgeCount ?? 0, + backgroundColor: context.flowColors.expense, + isLabelVisible: (badgeCount ?? 0) > 0, + ), + ), + ], + ), + action: TextButton.icon( + onPressed: () => context.push("/transactions/pending"), + label: Text("tabs.home.pendingTransactions.seeAll".t(context)), + icon: Icon(Symbols.arrow_right_alt_rounded), + iconAlignment: IconAlignment.end, + ), + ); + } +} diff --git a/lib/widgets/transaction_filter_head/select_multi_category_sheet.dart b/lib/widgets/transaction_filter_head/select_multi_category_sheet.dart index 12a1d6f..a42f41d 100644 --- a/lib/widgets/transaction_filter_head/select_multi_category_sheet.dart +++ b/lib/widgets/transaction_filter_head/select_multi_category_sheet.dart @@ -68,9 +68,6 @@ class _SelectMultiCategorySheetState extends State { title: Text(category.name), value: selectedUuids.contains(category.uuid), onChanged: (value) => select(category.uuid, value), - // leading: FlowIcon(category.icon), - // trailing: const Icon(Symbols.chevron_right_rounded), - // onTap: () => context.pop(Optional(category)), ), ), ], diff --git a/lib/widgets/transaction_list_tile.dart b/lib/widgets/transaction_list_tile.dart index fd3ca27..6471bf3 100644 --- a/lib/widgets/transaction_list_tile.dart +++ b/lib/widgets/transaction_list_tile.dart @@ -4,6 +4,7 @@ import "package:flow/entity/transaction/extensions/default/transfer.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/theme/theme.dart"; +import "package:flow/utils/extensions/transaction.dart"; import "package:flow/widgets/general/flow_icon.dart"; import "package:flow/widgets/general/money_text.dart"; import "package:flutter/material.dart"; @@ -25,12 +26,18 @@ class TransactionListTile extends StatelessWidget { final bool? overrideObscure; + /// moment_dart format string + /// + /// Defaults to "LT" + final String dateFormat; + const TransactionListTile({ super.key, required this.transaction, required this.deleteFn, required this.combineTransfers, this.padding = EdgeInsets.zero, + this.dateFormat = "LT", this.confirmFn, this.dismissibleKey, this.overrideObscure, @@ -38,15 +45,10 @@ class TransactionListTile extends StatelessWidget { @override Widget build(BuildContext context) { - final bool showPendingConfirmation = confirmFn != null && - transaction.isPending == true && - transaction.transactionDate - .isPastAnchored(Moment.now().endOfNextMinute()); + final bool showPendingConfirmation = + confirmFn != null && transaction.confirmable(); - final bool showHoldButton = confirmFn != null && - transaction.isPending != true && - transaction.transactionDate - .isFutureAnchored(Moment.now().startOfMinute()); + final bool showHoldButton = confirmFn != null && transaction.holdable(); if ((combineTransfers || showPendingConfirmation) && transaction.isTransfer && @@ -108,6 +110,7 @@ class TransactionListTile extends StatelessWidget { : transaction.title!), ), ], + style: context.textTheme.bodyMedium, ), maxLines: 3, overflow: TextOverflow.ellipsis, @@ -117,7 +120,8 @@ class TransactionListTile extends StatelessWidget { (transaction.isTransfer && combineTransfers) ? "${AccountActions.nameByUuid(transfer!.fromAccountUuid)} → ${AccountActions.nameByUuid(transfer.toAccountUuid)}" : transaction.account.target?.name, - transaction.transactionDate.format(payload: "LT"), + transaction.transactionDate + .format(payload: dateFormat), if (transaction.transactionDate.isFuture) transaction.isPending == true ? "transaction.pending".t(context) diff --git a/lib/widgets/home/transactions_date_header.dart b/lib/widgets/transactions_date_header.dart similarity index 51% rename from lib/widgets/home/transactions_date_header.dart rename to lib/widgets/transactions_date_header.dart index c45c01b..685d7aa 100644 --- a/lib/widgets/home/transactions_date_header.dart +++ b/lib/widgets/transactions_date_header.dart @@ -1,8 +1,11 @@ +import "package:flow/data/exchange_rates.dart"; import "package:flow/data/money.dart"; +import "package:flow/data/money_flow.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; +import "package:flow/services/exchange_rates.dart"; import "package:flow/theme/theme.dart"; import "package:flutter/widgets.dart"; import "package:moment_dart/moment_dart.dart"; @@ -18,11 +21,14 @@ class TransactionListDateHeader extends StatefulWidget { final bool resolveNonPrimaryCurrencies; + final Widget? titleOverride; + const TransactionListDateHeader({ super.key, required this.transactions, required this.range, this.action, + this.titleOverride, this.pendingGroup = false, this.resolveNonPrimaryCurrencies = true, }); @@ -30,6 +36,7 @@ class TransactionListDateHeader extends StatefulWidget { super.key, required this.range, this.action, + this.titleOverride, this.resolveNonPrimaryCurrencies = true, }) : pendingGroup = true, transactions = const []; @@ -59,45 +66,64 @@ class _TransactionListDateHeaderState extends State { @override Widget build(BuildContext context) { - final Widget title = Text( - widget.pendingGroup - ? "transactions.pending".t(context) - : _getRangeTitle(widget.range), - style: context.textTheme.headlineSmall, - ); + final Widget title = + widget.titleOverride ?? Text(_getRangeTitle(widget.range)); final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); - final double flow = widget.transactions - .where((transaction) => transaction.currency == primaryCurrency) - .sumWithoutCurrency; + final MoneyFlow flow = MoneyFlow() + ..addAll(widget.transactions.map((transaction) => transaction.money)); + final bool containsNonPrimaryCurrency = widget.transactions .any((transaction) => transaction.currency != primaryCurrency); - return Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + return ValueListenableBuilder( + valueListenable: ExchangeRatesService().exchangeRatesCache, + builder: (context, exchangeRatesCache, child) { + final ExchangeRates? rates = exchangeRatesCache?.get(primaryCurrency); + + final bool resolve = widget.resolveNonPrimaryCurrencies && + containsNonPrimaryCurrency && + rates != null; + + final String exclamation = + switch ((containsNonPrimaryCurrency, resolve)) { + (true, true) => "~", + (true, false) => "+ more", + _ => "", + }; + + final Money sum = resolve + ? flow.getTotalFlow(rates, primaryCurrency) + : flow.getFlowByCurrency(primaryCurrency); + + return Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - title, - if (!widget.pendingGroup) - Text( - "${Money(flow, primaryCurrency).formattedCompact}${containsNonPrimaryCurrency ? '+' : ''} • ${'tabs.home.transactionsCount'.t(context, widget.transactions.renderableCount)}", - style: context.textTheme.labelMedium, + Flexible( + fit: FlexFit.tight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + DefaultTextStyle( + style: context.textTheme.headlineSmall!, + child: title, + ), + if (!widget.pendingGroup) + Text( + "${sum.formattedCompact}$exclamation • ${'tabs.home.transactionsCount'.t(context, widget.transactions.renderableCount)}", + style: context.textTheme.labelMedium, + ), + ], ), + ), + if (widget.action != null) widget.action!, ], - ), - const SizedBox(width: 16.0), - Spacer(), - if (widget.action != null) - Flexible( - fit: FlexFit.tight, - child: widget.action!, - ), - ], + ); + }, ); } diff --git a/pubspec.lock b/pubspec.lock index 74982d2..964b411 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -290,10 +290,10 @@ packages: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: dependency: transitive description: @@ -378,10 +378,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" + sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" url: "https://pub.dev" source: hosted - version: "0.69.0" + version: "0.69.2" flat_buffers: dependency: transitive description: @@ -492,10 +492,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: f0e599ba89c9946c8e051780f0ec99aba4ba15895e0380a7ab68f420046fc44e + sha256: "999a4e3cb3e1532a971c86d6c73a480264f6a687959d4887cb4e2990821827e4" url: "https://pub.dev" source: hosted - version: "0.7.4+1" + version: "0.7.4+2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -614,10 +614,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "8ae664a70174163b9f65ea68dd8673e29db8f9095de7b5cd00e167c621f4fef5" + sha256: "8660b74171fafae4aa8202100fa2e55349e078281dadc73a241eb8e758534d9d" url: "https://pub.dev" source: hosted - version: "14.6.0" + version: "14.6.1" graphs: dependency: transitive description: @@ -766,10 +766,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" latlong2: dependency: "direct main" description: @@ -838,10 +838,10 @@ packages: dependency: transitive description: name: logger - sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" logging: dependency: transitive description: @@ -894,10 +894,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: "7b723abea4ad37e16fe921f1f1971cbb9b0f66d223a8c99981168a2306416b98" + sha256: "1dea2aef1c83434f832f14341a5ffa1254e76b68e4d90333f95f8a2643bf1024" url: "https://pub.dev" source: hosted - version: "4.2791.1" + version: "4.2799.0" meta: dependency: transitive description: @@ -1254,10 +1254,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" simple_icons: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a70f064..d421ab5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: dynamic_color: ^1.7.0 file_picker: ^8.0.7 file_saver: ^0.2.9 - fl_chart: ^0.69.0 + fl_chart: ^0.69.2 flutter: sdk: flutter flutter_dynamic_icon_plus: ^1.2.1 @@ -28,13 +28,13 @@ dependencies: flutter_localizations: sdk: flutter flutter_map: ^7.0.2 - flutter_markdown: ^0.7.3+1 + flutter_markdown: ^0.7.4+2 flutter_slidable: ^3.0.1 flutter_staggered_grid_view: ^0.7.0 flutter_typeahead: ^5.2.0 fuzzywuzzy: ^1.2.0 geolocator: ^13.0.1 - go_router: ^14.2.1 + go_router: ^14.6.1 http: ^1.2.2 image_picker: ^1.1.1 intl: ^0.19.0 @@ -43,7 +43,7 @@ dependencies: local_hero: ^0.3.0 local_settings: ^0.5.0 mask_text_input_formatter: ^2.8.0 - material_symbols_icons: ^4.2719.1 + material_symbols_icons: ^4.2799.0 moment_dart: ^2.2.1+beta.0 objectbox: ^4.0.3 objectbox_flutter_libs: ^4.0.3 From 0980f44a89d0a132c25578e413552c71d88531c6 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Nov 2024 14:54:07 +0800 Subject: [PATCH 07/19] update lock --- pubspec.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 964b411..3530452 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -290,10 +290,10 @@ packages: dependency: transitive description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.5" fake_async: dependency: transitive description: @@ -766,10 +766,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.9.0" + version: "6.8.0" latlong2: dependency: "direct main" description: @@ -838,10 +838,10 @@ packages: dependency: transitive description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.4.0" logging: dependency: transitive description: @@ -1254,10 +1254,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.0" simple_icons: dependency: "direct main" description: From 8bbd264f524e83e1c9d5775f39da3d9750ec7a8b Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Nov 2024 15:27:32 +0800 Subject: [PATCH 08/19] bump version --- CHANGELOG.md | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca67f9e..c665931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Beta next +## Beta next (0.9.0) ### New features diff --git a/pubspec.yaml b/pubspec.yaml index d421ab5..c490f8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.8.1+81" +version: "0.9.0+82" environment: sdk: ">=3.5.0 <4.0.0" From 62b998e0af66e32b63d3144e3be1e9c6e330cb54 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 24 Nov 2024 15:46:01 +0800 Subject: [PATCH 09/19] fix: l10n --- lib/routes/preferences/home_tab_preferences.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/routes/preferences/home_tab_preferences.dart b/lib/routes/preferences/home_tab_preferences.dart index c51324e..eb3dcec 100644 --- a/lib/routes/preferences/home_tab_preferences.dart +++ b/lib/routes/preferences/home_tab_preferences.dart @@ -30,7 +30,7 @@ class _HomeTabPreferencesPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16.0), - ListHeader("preferences.home.upcoming".t(context)), + ListHeader("preferences.home.pending".t(context)), const SizedBox(height: 8.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -59,7 +59,7 @@ class _HomeTabPreferencesPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 12.0), child: InfoText( child: - Text("preferences.home.upcoming.description".t(context)), + Text("preferences.home.pending.description".t(context)), ), ), const SizedBox(height: 16.0), From cba2bfbc112bfd237303567b0cec16fc20c44ce8 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Mon, 25 Nov 2024 01:07:13 +0800 Subject: [PATCH 10/19] Enhancements --- assets/l10n/en_IN.json | 17 ++-- assets/l10n/en_US.json | 17 ++-- assets/l10n/it_IT.json | 17 ++-- assets/l10n/mn_MN.json | 17 ++-- lib/data/pending_transactions_duration.dart | 45 ----------- lib/objectbox/actions.dart | 8 +- lib/prefs.dart | 28 +++---- lib/routes.dart | 7 +- lib/routes/home/home_tab.dart | 23 +++--- .../preferences/home_tab_preferences.dart | 79 ------------------- .../preferences/pending_transactions.dart | 74 ++++++++++++++++- ...age.dart => privacy_preferences_page.dart} | 14 +++- lib/routes/preferences_page.dart | 10 +-- .../transaction_context_actions.dart | 6 +- lib/widgets/transaction_list_tile.dart | 20 +++-- 15 files changed, 162 insertions(+), 220 deletions(-) delete mode 100644 lib/data/pending_transactions_duration.dart delete mode 100644 lib/routes/preferences/home_tab_preferences.dart rename lib/routes/preferences/{startup_privacy_preferences_page.dart => privacy_preferences_page.dart} (74%) diff --git a/assets/l10n/en_IN.json b/assets/l10n/en_IN.json index 8a17ebe..398916d 100644 --- a/assets/l10n/en_IN.json +++ b/assets/l10n/en_IN.json @@ -29,6 +29,7 @@ "general.enabled": "Enabled", "general.disabled": "Disabled", "general.selectLocation": "Choose location", + "general.nextNDays": "Next {} day(s)", "setup.getStarted": "Get started", "setup.next": "Next", @@ -176,9 +177,6 @@ "preferences.transfer.combineTransferTransaction.filterDescription": "When using filters, transfers will always display separately", "preferences.transfer.excludeTransferFromFlow": "Exclude from totals", "preferences.transfer.excludeTransferFromFlow.description": "Don't count towards total expense/income", - "preferences.home": "Home page", - "preferences.home.pending": "Planned transactions", - "preferences.home.pending.description": "Shows upcoming transactions for the selected duration", "preferences.transactionGeo": "Transaction location", "preferences.transactionGeo.enable": "Enable", "preferences.transactionGeo.disableInstructions": "You can hide this section in settings", @@ -187,10 +185,14 @@ "preferences.transactionGeo.auto.description": "Automatically attach your current location to new transactions. Even if you have this turned off, you can still choose a location on a map to attach it.", "preferences.transactionGeo.auto.permissionDenied": "Location permission was denied. You can enable it from your settings if you wish.", "preferences.pendingTransactions": "Pending transactions", + "preferences.pendingTransactions.homeTimeframe": "Show on home", "preferences.pendingTransactions.requireConfirmation": "Require confirmation", "preferences.pendingTransactions.requireConfirmation.description": "Pending transactions will not be counted towards income, expenses, and account balance", - "preferences.startupPrivacyMode": "Privacy mode", - "preferences.startupPrivacyMode.description": "Enable privacy mode when the app start", + "preferences.pendingTransactions.updateDateUponConfirmation": "Update date upon confirmation", + "preferences.pendingTransactions.updateDateUponConfirmation.description": "Disable to retain original transaction date", + "preferences.privacyMode": "Privacy mode", + "preferences.privacyMode.description": "Nasconde importi. Attiva/disattiva con l'icona dell'occhio nell'angolo in alto a destra della schermata iniziale", + "preferences.privacyMode.enableAtStartup": "Enable at startup", "preferences.moneyFormatting": "Money formatting", "preferences.moneyFormatting.preferFull": "Prefer full amounts", "preferences.moneyFormatting.preferFull.description": "Don't abbreviate numbers when possible", @@ -307,11 +309,6 @@ "enum.TransactionType@income": "Income", "enum.TransactionType@expense": "Expense", "enum.TransactionType@transfer": "Transfer", - "enum.PendingTransactionsDuration@next1Days": "Next 1 day", - "enum.PendingTransactionsDuration@next2Days": "Next 2 days", - "enum.PendingTransactionsDuration@next3Days": "Next 3 days", - "enum.PendingTransactionsDuration@next5Days": "Next 5 days", - "enum.PendingTransactionsDuration@next7Days": "Next 7 days", "enum.CSVHeadersV1": "CSV Headers", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index a4f48c4..d656e6d 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -29,6 +29,7 @@ "general.enabled": "Enabled", "general.disabled": "Disabled", "general.selectLocation": "Choose location", + "general.nextNDays": "Next {} day(s)", "setup.getStarted": "Get started", "setup.next": "Next", @@ -176,9 +177,6 @@ "preferences.transfer.combineTransferTransaction.filterDescription": "When using filters, transfers will always display separately", "preferences.transfer.excludeTransferFromFlow": "Exclude from totals", "preferences.transfer.excludeTransferFromFlow.description": "Don't count towards total expense/income", - "preferences.home": "Home page", - "preferences.home.pending": "Pending transactions", - "preferences.home.pending.description": "Shows upcoming transactions for the selected duration", "preferences.transactionGeo": "Transaction location", "preferences.transactionGeo.enable": "Enable", "preferences.transactionGeo.disableInstructions": "You can hide this section in settings", @@ -187,10 +185,14 @@ "preferences.transactionGeo.auto.description": "Automatically attach your current location to new transactions. Even if you have this turned off, you can still choose a location on a map to attach it.", "preferences.transactionGeo.auto.permissionDenied": "Location permission was denied. You can enable it from your settings if you wish.", "preferences.pendingTransactions": "Pending transactions", + "preferences.pendingTransactions.homeTimeframe": "Show on home", "preferences.pendingTransactions.requireConfirmation": "Require confirmation", "preferences.pendingTransactions.requireConfirmation.description": "Pending transactions will not be counted towards income, expenses, and account balance", - "preferences.startupPrivacyMode": "Privacy mode", - "preferences.startupPrivacyMode.description": "Enable privacy mode when the app start", + "preferences.pendingTransactions.updateDateUponConfirmation": "Update date upon confirmation", + "preferences.pendingTransactions.updateDateUponConfirmation.description": "Disable to retain original transaction date", + "preferences.privacyMode": "Privacy mode", + "preferences.privacyMode.description": "Obscures amounts. Toggle with the eye icon at the upper right corner of home screen", + "preferences.privacyMode.enableAtStartup": "Enable at startup", "preferences.moneyFormatting": "Money formatting", "preferences.moneyFormatting.preferFull": "Prefer full amounts", "preferences.moneyFormatting.preferFull.description": "Don't abbreviate numbers when possible", @@ -307,11 +309,6 @@ "enum.TransactionType@income": "Income", "enum.TransactionType@expense": "Expense", "enum.TransactionType@transfer": "Transfer", - "enum.PendingTransactionsDuration@next1Days": "Next 1 day", - "enum.PendingTransactionsDuration@next2Days": "Next 2 days", - "enum.PendingTransactionsDuration@next3Days": "Next 3 days", - "enum.PendingTransactionsDuration@next5Days": "Next 5 days", - "enum.PendingTransactionsDuration@next7Days": "Next 7 days", "enum.CSVHeadersV1": "CSV Headers", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index c695193..6fe071c 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -29,6 +29,7 @@ "general.enabled": "Abilitato", "general.disabled": "Disabilitato", "general.selectLocation": "Scegli la posizione", + "general.nextNDays": "Prossimi {} giorni", "setup.getStarted": "Iniziare", "setup.next": "Avanti", @@ -176,9 +177,6 @@ "preferences.transfer.combineTransferTransaction.filterDescription": "Utilizzando i filtri, i trasferimenti verranno sempre visualizzati separatamente", "preferences.transfer.excludeTransferFromFlow": "Escludi dai totali", "preferences.transfer.excludeTransferFromFlow.description": "Non conteggiare verso la spesa/entrata totale", - "preferences.home": "Home page", - "preferences.home.pending": "Transazioni in sospeso", - "preferences.home.pending.description": "Mostra le transazioni future per il periodo selezionato", "preferences.transactionGeo": "Posizione transazione", "preferences.transactionGeo.enable": "Abilita", "preferences.transactionGeo.disableInstructions": "Puoi nascondere questa sezione nelle impostazioni.", @@ -187,10 +185,14 @@ "preferences.transactionGeo.auto.description": "Allega automaticamente la tua posizione attuale alle nuove transazioni. Anche se hai disattivato questa funzione, puoi comunque scegliere una posizione su una mappa per allegarla.", "preferences.transactionGeo.auto.permissionDenied": "L'autorizzazione per la posizione è stata negata. Puoi abilitarmi dalle tue impostazioni se lo desideri.", "preferences.pendingTransactions": "Transazioni in sospeso", + "preferences.pendingTransactions.homeTimeframe": "Mostra in Home", "preferences.pendingTransactions.requireConfirmation": "Richiedi conferma", "preferences.pendingTransactions.requireConfirmation.description": "Le transazioni in sospeso non verranno conteggiate nel reddito, nelle spese e nel saldo del conto", - "preferences.startupPrivacyMode": "Modalità privacy", - "preferences.startupPrivacyMode.description": "Abilita la modalità privacy all'avvio dell'app", + "preferences.pendingTransactions.updateDateUponConfirmation": "Aggiorna data alla conferma", + "preferences.pendingTransactions.updateDateUponConfirmation.description": "Disattiva per mantenere la data di transazione originale", + "preferences.privacyMode": "Modalità privacy", + "preferences.privacyMode.description": "Дүнг одолж харуулна. Нүүр хуудасны баруун дээр байрлах нүдэн дээр дарж асаах/унтраах боломжтой", + "preferences.privacyMode.enableAtStartup": "Attiva all'avvio", "preferences.moneyFormatting": "Formato valuta", "preferences.moneyFormatting.preferFull": "Preferisci importo completo", "preferences.moneyFormatting.preferFull.description": "Non abbreviare i numeri quando possibile", @@ -307,11 +309,6 @@ "enum.TransactionType@income": "Entrata", "enum.TransactionType@expense": "Uscita", "enum.TransactionType@transfer": "Trasferimento", - "enum.PendingTransactionsDuration@next1Days": "Prossimo giorno", - "enum.PendingTransactionsDuration@next2Days": "Prossimi 2 giorni", - "enum.PendingTransactionsDuration@next3Days": "Prossimi 3 giorni", - "enum.PendingTransactionsDuration@next5Days": "Prossimi 5 giorni", - "enum.PendingTransactionsDuration@next7Days": "Prossimi 7 giorni", "enum.CSVHeadersV1": "Intestazioni CSV", "enum.CSVHeadersV1@uuid": "ID", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index 86235ec..be3fb54 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -29,6 +29,7 @@ "general.enabled": "Идэвхтэй", "general.disabled": "Идэвхгүй", "general.selectLocation": "Байршил сонгох", + "general.nextNDays": "Ирэх {} хоног", "setup.getStarted": "Эхэлцгээе", "setup.next": "Үргэлжлүүлэх", @@ -176,9 +177,6 @@ "preferences.transfer.combineTransferTransaction.filterDescription": "Шүүлтүүр ашиглаж байх үед үргэлж салангид харагдах болно", "preferences.transfer.excludeTransferFromFlow": "Нийт дүнд оруулахгүй", "preferences.transfer.excludeTransferFromFlow.description": "Идэвхтэй үед орлого/зарлага-д тоолохгүй", - "preferences.home": "Нүүр хуудас", - "preferences.home.pending": "Хүлээгдэж буй гүйлгээнүүд", - "preferences.home.pending.description": "Сонгосон хугацаа дахь гүйлгээнүүдийг харуулна", "preferences.transactionGeo": "Гүйлгээний байршил", "preferences.transactionGeo.enable": "Идэвхжүүлэх", "preferences.transactionGeo.disableInstructions": "Энэ хэсгийг тохиргооноос нуух боломжтой", @@ -189,8 +187,12 @@ "preferences.pendingTransactions": "Хүлээгдэж буй гүйлгээнүүд", "preferences.pendingTransactions.requireConfirmation": "Баталгаажуулалт шаардах", "preferences.pendingTransactions.requireConfirmation.description": "Баталгаажуулаагүй гүйлгээнүүд орлого, зарлага, дансны үлдэгдэлд тооцогдохгүй", - "preferences.startupPrivacyMode": "Нууцлалын горим", - "preferences.startupPrivacyMode.description": "Апп нээх үед нууцлалын горимыг идэвхжүүлэх", + "preferences.pendingTransactions.updateDateUponConfirmation": "Гүйлгээ баталгаажуулах үед огноо шинэчлэх", + "preferences.pendingTransactions.updateDateUponConfirmation.description": "Идэвхгүй болгож анхны огноог ашиглах боломжтой", + "preferences.pendingTransactions.homeTimeframe": "Нүүр хуудсанд харуулах", + "preferences.privacyMode": "Нууцлалын горим", + "preferences.privacyMode.description": "Дүнг одолж харуулна. Нүүр хуудасны баруун дээр байрлах нүдэн дээр дарж асаах/унтраах боломжтой", + "preferences.privacyMode.enableAtStartup": "Эхлэх үед идэвхжүүлэх", "preferences.moneyFormatting": "Мөнгөний хэлбэр", "preferences.moneyFormatting.preferFull": "Бүтэн дүнг харуулах", "preferences.moneyFormatting.preferFull.description": "Тоог хураахгүй бүтнээр нь харуулах", @@ -307,11 +309,6 @@ "enum.TransactionType@income": "Орлого", "enum.TransactionType@expense": "Зарлага", "enum.TransactionType@transfer": "Шилжүүлэг", - "enum.PendingTransactionsDuration@next1Days": "Ирэх 1 хоног", - "enum.PendingTransactionsDuration@next2Days": "Ирэх 2 хоног", - "enum.PendingTransactionsDuration@next3Days": "Ирэх 3 хоног", - "enum.PendingTransactionsDuration@next5Days": "Ирэх 5 хоног", - "enum.PendingTransactionsDuration@next7Days": "Ирэх 7 хоног", "enum.CSVHeadersV1": "CSV Толгой", "enum.CSVHeadersV1@uuid": "Код", diff --git a/lib/data/pending_transactions_duration.dart b/lib/data/pending_transactions_duration.dart deleted file mode 100644 index 64cc406..0000000 --- a/lib/data/pending_transactions_duration.dart +++ /dev/null @@ -1,45 +0,0 @@ -import "package:flow/l10n/named_enum.dart"; -import "package:flow/utils/utils.dart"; -import "package:json_annotation/json_annotation.dart"; -import "package:moment_dart/moment_dart.dart"; - -@JsonEnum(valueField: "value") -enum PendingTransactionsDuration implements LocalizedEnum { - next1Days("next1Days"), - next2Days("next2Days"), - next3Days("next3Days"), - next5Days("next5Days"), - next7Days("next7Days"); - - final String value; - - const PendingTransactionsDuration(this.value); - - @override - String get localizationEnumValue => name; - @override - String get localizationEnumName => "PendingTransactionsDuration"; - - DateTime? endsAt([DateTime? anchor]) { - final Moment now = anchor?.toMoment() ?? Moment.now(); - switch (this) { - case PendingTransactionsDuration.next1Days: - return now.add(Duration(days: 1)).endOfDay(); - case PendingTransactionsDuration.next2Days: - return now.add(Duration(days: 2)).endOfDay(); - case PendingTransactionsDuration.next3Days: - return now.add(Duration(days: 3)).endOfDay(); - case PendingTransactionsDuration.next5Days: - return now.add(Duration(days: 5)).endOfDay(); - case PendingTransactionsDuration.next7Days: - return now.add(Duration(days: 7)).endOfDay(); - } - } - - static PendingTransactionsDuration? fromJson(Map json) { - return PendingTransactionsDuration.values - .firstWhereOrNull((element) => element.value == json["value"]); - } - - Map toJson() => {"value": value}; -} diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index 787eb27..8184819 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -413,7 +413,7 @@ extension TransactionActions on Transaction { return ObjectBox().box().remove(id); } - bool confirm([bool confirm = true]) { + bool confirm([bool confirm = true, bool updateTransactionDate = true]) { try { if (isTransfer) { final Transfer? transfer = extensions.transfer; @@ -438,6 +438,9 @@ extension TransactionActions on Transaction { } relatedTransaction.isPending = !confirm; + if (updateTransactionDate && isPending != true) { + relatedTransaction.transactionDate = Moment.now(); + } ObjectBox() .box() .put(relatedTransaction, mode: PutMode.update); @@ -448,6 +451,9 @@ extension TransactionActions on Transaction { } isPending = !confirm; + if (updateTransactionDate && isPending != true) { + transactionDate = Moment.now(); + } ObjectBox().box().put(this, mode: PutMode.update); return true; } catch (e) { diff --git a/lib/prefs.dart b/lib/prefs.dart index 5dfcf3e..70ec5b8 100644 --- a/lib/prefs.dart +++ b/lib/prefs.dart @@ -4,7 +4,6 @@ import "dart:developer"; import "package:flow/data/exchange_rates_set.dart"; import "package:flow/data/prefs/frecency.dart"; -import "package:flow/data/pending_transactions_duration.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/category.dart"; import "package:flow/entity/transaction.dart"; @@ -22,9 +21,7 @@ import "package:shared_preferences/shared_preferences.dart"; class LocalPreferences { final SharedPreferences _prefs; - static const PendingTransactionsDuration - homeTabPlannedTransactionsDurationDefault = - PendingTransactionsDuration.next3Days; + static const int pendingTransactionsHomeTimeframeDefault = 3; /// Main currency used in the app late final PrimitiveSettingsEntry primaryCurrency; @@ -53,8 +50,11 @@ class LocalPreferences { late final BoolSettingsEntry excludeTransferFromFlow; /// Shows next [homeTabPlannedTransactionsDays] days of planned transactions in the home tab - late final JsonSettingsEntry - homeTabPlannedTransactionsDuration; + late final PrimitiveSettingsEntry pendingTransactionsHomeTimeframe; + + /// Whether to use date of confirmation for `transactionDate` for pending transactions + late final BoolSettingsEntry pendingTransactionsUpdateDateUponConfirmation; + late final JsonListSettingsEntry transactionButtonOrder; late final BoolSettingsEntry completedInitialSetup; @@ -111,15 +111,15 @@ class LocalPreferences { preferences: _prefs, initialValue: false, ); - homeTabPlannedTransactionsDuration = - JsonSettingsEntry( - key: "homeTabPlannedTransactionsDuration", + pendingTransactionsHomeTimeframe = PrimitiveSettingsEntry( + key: "pendingTransactions.homeTimeframe", preferences: _prefs, - initialValue: homeTabPlannedTransactionsDurationDefault, - fromJson: (map) => - PendingTransactionsDuration.fromJson(map) ?? - homeTabPlannedTransactionsDurationDefault, - toJson: (data) => data.toJson(), + initialValue: pendingTransactionsHomeTimeframeDefault, + ); + pendingTransactionsUpdateDateUponConfirmation = BoolSettingsEntry( + key: "pendingTransactions.updateDateUponConfirmation", + preferences: _prefs, + initialValue: true, ); transactionButtonOrder = JsonListSettingsEntry( key: "transactionButtonOrder", diff --git a/lib/routes.dart b/lib/routes.dart index a830bb9..a73fd50 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -13,11 +13,10 @@ import "package:flow/routes/home_page.dart"; import "package:flow/routes/import_page.dart"; import "package:flow/routes/import_wizard/v1.dart"; import "package:flow/routes/preferences/button_order_preferences_page.dart"; -import "package:flow/routes/preferences/home_tab_preferences.dart"; import "package:flow/routes/preferences/money_formatting_preferences_page.dart"; import "package:flow/routes/preferences/numpad_preferences_page.dart"; import "package:flow/routes/preferences/pending_transactions.dart"; -import "package:flow/routes/preferences/startup_privacy_preferences_page.dart"; +import "package:flow/routes/preferences/privacy_preferences_page.dart"; import "package:flow/routes/preferences/theme_preferences_page.dart"; import "package:flow/routes/preferences/transaction_geo_preferences_page.dart"; import "package:flow/routes/preferences/transfer_preferences_page.dart"; @@ -144,10 +143,6 @@ final router = GoRouter( path: "/preferences", builder: (context, state) => const PreferencesPage(), routes: [ - GoRoute( - path: "home", - builder: (context, state) => const HomeTabPreferencesPage(), - ), GoRoute( path: "pendingTransactions", builder: (context, state) => diff --git a/lib/routes/home/home_tab.dart b/lib/routes/home/home_tab.dart index 59a53ad..c2750ea 100644 --- a/lib/routes/home/home_tab.dart +++ b/lib/routes/home/home_tab.dart @@ -1,6 +1,5 @@ import "package:flow/data/exchange_rates.dart"; import "package:flow/data/transactions_filter.dart"; -import "package:flow/data/pending_transactions_duration.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; @@ -31,8 +30,7 @@ class HomeTab extends StatefulWidget { class _HomeTabState extends State with AutomaticKeepAliveClientMixin { late final AppLifecycleListener _listener; - PendingTransactionsDuration _plannedTransactionsDuration = - LocalPreferences.homeTabPlannedTransactionsDurationDefault; + late int _plannedTransactionsNextNDays; final TransactionFilter defaultFilter = TransactionFilter( range: last30Days(), @@ -41,10 +39,9 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { late TransactionFilter currentFilter = defaultFilter.copyWithOptional(); TransactionFilter get currentFilterWithPlanned { - final DateTime? plannedTransactionTo = - _plannedTransactionsDuration.endsAt(); - - if (plannedTransactionTo == null) return currentFilter; + final DateTime plannedTransactionTo = Moment.now() + .add(Duration(days: _plannedTransactionsNextNDays)) + .startOfNextDay(); if (currentFilter.range != null && currentFilter.range!.contains(Moment.now()) && @@ -69,7 +66,7 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { super.initState(); _updatePlannedTransactionDays(); LocalPreferences() - .homeTabPlannedTransactionsDuration + .pendingTransactionsHomeTimeframe .addListener(_updatePlannedTransactionDays); _listener = AppLifecycleListener(onShow: () => setState(() {})); @@ -79,7 +76,7 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { void dispose() { _listener.dispose(); LocalPreferences() - .homeTabPlannedTransactionsDuration + .pendingTransactionsHomeTimeframe .removeListener(_updatePlannedTransactionDays); super.dispose(); } @@ -99,7 +96,7 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { event.find().filter(currentFilterWithPlanned.postPredicates), ), builder: (context, snapshot) { - final DateTime now = DateTime.now().startOfNextMinute(); + final DateTime now = Moment.now().startOfNextMinute(); final ExchangeRates? rates = ExchangeRatesService().getPrimaryCurrencyRates(); final List? transactions = snapshot.data; @@ -221,9 +218,9 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { } void _updatePlannedTransactionDays() { - _plannedTransactionsDuration = - LocalPreferences().homeTabPlannedTransactionsDuration.get() ?? - LocalPreferences.homeTabPlannedTransactionsDurationDefault; + _plannedTransactionsNextNDays = + LocalPreferences().pendingTransactionsHomeTimeframe.get() ?? + LocalPreferences.pendingTransactionsHomeTimeframeDefault; setState(() {}); } diff --git a/lib/routes/preferences/home_tab_preferences.dart b/lib/routes/preferences/home_tab_preferences.dart deleted file mode 100644 index eb3dcec..0000000 --- a/lib/routes/preferences/home_tab_preferences.dart +++ /dev/null @@ -1,79 +0,0 @@ -import "package:flow/data/pending_transactions_duration.dart"; -import "package:flow/l10n/extensions.dart"; -import "package:flow/l10n/named_enum.dart"; -import "package:flow/prefs.dart"; -import "package:flow/widgets/general/info_text.dart"; -import "package:flow/widgets/general/list_header.dart"; -import "package:flutter/material.dart"; - -class HomeTabPreferencesPage extends StatefulWidget { - const HomeTabPreferencesPage({super.key}); - - @override - State createState() => _HomeTabPreferencesPageState(); -} - -class _HomeTabPreferencesPageState extends State { - @override - Widget build(BuildContext context) { - final PendingTransactionsDuration homeTabPlannedTransactionsDuration = - LocalPreferences().homeTabPlannedTransactionsDuration.get() ?? - LocalPreferences.homeTabPlannedTransactionsDurationDefault; - - return Scaffold( - appBar: AppBar( - title: Text("preferences.home".t(context)), - ), - body: SafeArea( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16.0), - ListHeader("preferences.home.pending".t(context)), - const SizedBox(height: 8.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Wrap( - spacing: 12.0, - runSpacing: 8.0, - children: PendingTransactionsDuration.values - .map( - (value) => FilterChip( - showCheckmark: false, - key: ValueKey(value.value), - label: Text( - value.localizedNameContext(context), - ), - onSelected: (bool selected) => selected - ? updateHomeTabPlannedTransactionsDays(value) - : null, - selected: value == homeTabPlannedTransactionsDuration, - ), - ) - .toList(), - ), - ), - const SizedBox(height: 8.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: InfoText( - child: - Text("preferences.home.pending.description".t(context)), - ), - ), - const SizedBox(height: 16.0), - ], - ), - ), - ), - ); - } - - void updateHomeTabPlannedTransactionsDays( - PendingTransactionsDuration duration) async { - await LocalPreferences().homeTabPlannedTransactionsDuration.set(duration); - - if (mounted) setState(() {}); - } -} diff --git a/lib/routes/preferences/pending_transactions.dart b/lib/routes/preferences/pending_transactions.dart index 22a15c7..883d73e 100644 --- a/lib/routes/preferences/pending_transactions.dart +++ b/lib/routes/preferences/pending_transactions.dart @@ -1,6 +1,7 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/prefs.dart"; import "package:flow/widgets/general/info_text.dart"; +import "package:flow/widgets/general/list_header.dart"; import "package:flutter/material.dart"; class PendingTransactionPreferencesPage extends StatefulWidget { @@ -15,8 +16,13 @@ class _PendingTransactionPreferencesPageState extends State { @override Widget build(BuildContext context) { + final int pendingTransactionsHomeTimeframe = + LocalPreferences().pendingTransactionsHomeTimeframe.get() ?? + LocalPreferences.pendingTransactionsHomeTimeframeDefault; final bool requirePendingTransactionConfrimation = LocalPreferences().requirePendingTransactionConfrimation.get(); + final bool pendingTransactionsUpdateDateUponConfirmation = + LocalPreferences().pendingTransactionsUpdateDateUponConfirmation.get(); return Scaffold( appBar: AppBar( @@ -27,10 +33,38 @@ class _PendingTransactionPreferencesPageState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 16.0), + ListHeader( + "preferences.pendingTransactions.homeTimeframe".t(context), + ), + const SizedBox(height: 8.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Wrap( + spacing: 12.0, + runSpacing: 8.0, + children: [1, 2, 3, 5, 7, 14, 30] + .map( + (value) => FilterChip( + showCheckmark: false, + key: ValueKey(value), + label: Text( + "general.nextNDays".t(context, value), + ), + onSelected: (bool selected) => selected + ? updatePendingTransactionsHomeTimeframe(value) + : null, + selected: value == pendingTransactionsHomeTimeframe, + ), + ) + .toList(), + ), + ), CheckboxListTile.adaptive( title: Text( - "preferences.pendingTransactions.requireConfirmation" - .t(context)), + "preferences.pendingTransactions.requireConfirmation" + .t(context), + ), value: requirePendingTransactionConfrimation, onChanged: updateRequirePendingTransactionConfrimation, ), @@ -44,6 +78,21 @@ class _PendingTransactionPreferencesPageState ), ), ), + const SizedBox(height: 16.0), + if (requirePendingTransactionConfrimation) ...[ + CheckboxListTile.adaptive( + title: Text( + "preferences.pendingTransactions.updateDateUponConfirmation" + .t(context), + ), + subtitle: Text( + "preferences.pendingTransactions.updateDateUponConfirmation.description" + .t(context), + ), + value: pendingTransactionsUpdateDateUponConfirmation, + onChanged: updatePendingTransactionsConfirmationDate, + ), + ], ], ), ), @@ -51,8 +100,15 @@ class _PendingTransactionPreferencesPageState ); } + void updatePendingTransactionsHomeTimeframe(int days) async { + await LocalPreferences().pendingTransactionsHomeTimeframe.set(days); + + if (mounted) setState(() {}); + } + void updateRequirePendingTransactionConfrimation( - bool? requirePendingTransactionConfrimation) async { + bool? requirePendingTransactionConfrimation, + ) async { if (requirePendingTransactionConfrimation == null) return; await LocalPreferences() @@ -61,4 +117,16 @@ class _PendingTransactionPreferencesPageState if (mounted) setState(() {}); } + + void updatePendingTransactionsConfirmationDate( + bool? newValue, + ) async { + if (newValue == null) return; + + await LocalPreferences() + .pendingTransactionsUpdateDateUponConfirmation + .set(newValue); + + if (mounted) setState(() {}); + } } diff --git a/lib/routes/preferences/startup_privacy_preferences_page.dart b/lib/routes/preferences/privacy_preferences_page.dart similarity index 74% rename from lib/routes/preferences/startup_privacy_preferences_page.dart rename to lib/routes/preferences/privacy_preferences_page.dart index 8c78d3b..3098bb7 100644 --- a/lib/routes/preferences/startup_privacy_preferences_page.dart +++ b/lib/routes/preferences/privacy_preferences_page.dart @@ -1,5 +1,6 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/prefs.dart"; +import "package:flow/widgets/general/info_text.dart"; import "package:flutter/material.dart"; class StartupPrivacyPreferencesPage extends StatefulWidget { @@ -18,18 +19,23 @@ class _StartupPrivacyPreferencesPageState return Scaffold( appBar: AppBar( - title: Text("preferences.startupPrivacyMode".t(context)), + title: Text("preferences.privacyMode".t(context)), ), body: SafeArea( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 16.0), + InfoText( + child: Text( + "preferences.privacyMode.description".t(context), + ), + ), const SizedBox(height: 16.0), CheckboxListTile.adaptive( - title: Text("preferences.startupPrivacyMode".t(context)), - subtitle: Text( - "preferences.startupPrivacyMode.description".t(context)), + title: + Text("preferences.privacyMode.enableAtStartup".t(context)), value: privacyMode, onChanged: updatePrivacyMode, ), diff --git a/lib/routes/preferences_page.dart b/lib/routes/preferences_page.dart index cfb6f10..dec040c 100644 --- a/lib/routes/preferences_page.dart +++ b/lib/routes/preferences_page.dart @@ -2,9 +2,7 @@ import "dart:developer"; import "dart:io"; import "package:app_settings/app_settings.dart"; -import "package:flow/data/pending_transactions_duration.dart"; import "package:flow/l10n/flow_localizations.dart"; -import "package:flow/l10n/named_enum.dart"; import "package:flow/prefs.dart"; import "package:flow/routes/preferences/language_selection_sheet.dart"; import "package:flow/theme/color_themes/registry.dart"; @@ -30,9 +28,9 @@ class _PreferencesPageState extends State { final FlowColorScheme currentTheme = getTheme(LocalPreferences().themeName.get()); - final PendingTransactionsDuration homeTabPlannedTransactionsDuration = - LocalPreferences().homeTabPlannedTransactionsDuration.get() ?? - LocalPreferences.homeTabPlannedTransactionsDurationDefault; + final int pendingTransactionsHomeTimeframe = + LocalPreferences().pendingTransactionsHomeTimeframe.get() ?? + LocalPreferences.pendingTransactionsHomeTimeframeDefault; final bool enableGeo = LocalPreferences().enableGeo.get(); final bool autoAttachTransactionGeo = @@ -50,7 +48,7 @@ class _PreferencesPageState extends State { ListTile( title: Text("preferences.home.pending".t(context)), subtitle: Text( - homeTabPlannedTransactionsDuration.localizedNameContext(context), + "general.nextNDays".t(context, pendingTransactionsHomeTimeframe), ), leading: const Icon(Symbols.hourglass_top_rounded), onTap: () => pushAndRefreshAfter("/preferences/home"), diff --git a/lib/utils/extensions/transaction_context_actions.dart b/lib/utils/extensions/transaction_context_actions.dart index 8c0261c..5373c24 100644 --- a/lib/utils/extensions/transaction_context_actions.dart +++ b/lib/utils/extensions/transaction_context_actions.dart @@ -1,6 +1,7 @@ import "package:flow/entity/transaction.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; +import "package:flow/prefs.dart"; import "package:flow/utils/utils.dart"; import "package:flutter/widgets.dart"; @@ -23,6 +24,9 @@ extension TransactionContextActions on BuildContext { Transaction transaction, [ bool confirm = true, ]) async { - transaction.confirm(confirm); + final bool updateTransactionDate = + LocalPreferences().pendingTransactionsUpdateDateUponConfirmation.get(); + + transaction.confirm(confirm, updateTransactionDate); } } diff --git a/lib/widgets/transaction_list_tile.dart b/lib/widgets/transaction_list_tile.dart index 6471bf3..5c848a1 100644 --- a/lib/widgets/transaction_list_tile.dart +++ b/lib/widgets/transaction_list_tile.dart @@ -26,18 +26,12 @@ class TransactionListTile extends StatelessWidget { final bool? overrideObscure; - /// moment_dart format string - /// - /// Defaults to "LT" - final String dateFormat; - const TransactionListTile({ super.key, required this.transaction, required this.deleteFn, required this.combineTransfers, this.padding = EdgeInsets.zero, - this.dateFormat = "LT", this.confirmFn, this.dismissibleKey, this.overrideObscure, @@ -120,8 +114,7 @@ class TransactionListTile extends StatelessWidget { (transaction.isTransfer && combineTransfers) ? "${AccountActions.nameByUuid(transfer!.fromAccountUuid)} → ${AccountActions.nameByUuid(transfer.toAccountUuid)}" : transaction.account.target?.name, - transaction.transactionDate - .format(payload: dateFormat), + dateString, if (transaction.transactionDate.isFuture) transaction.isPending == true ? "transaction.pending".t(context) @@ -194,4 +187,15 @@ class TransactionListTile extends StatelessWidget { child: listTile, ); } + + String get dateString { + final DateTime now = Moment.now().startOfNextMinute(); + + final bool pending = transaction.isPending == true || + transaction.transactionDate.isFutureAnchored(now); + + if (pending) return transaction.transactionDate.toMoment().calendar(); + + return transaction.transactionDate.toMoment().LT; + } } From 8db568f0dda415bf6830e4ecf43a81b3172a9ea5 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Mon, 25 Nov 2024 01:09:13 +0800 Subject: [PATCH 11/19] update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c665931..49e9c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,12 @@ ### Changes * Renamed upcoming/planned -> Pending transactions -* Home tab pending transactions time range settings has been revised +* Home tab pending transactions time range settings has been revised. + You will need to update your preferences again. * Deprecated old theme system (light/dark). If you missed an update since this features was introduced, you will need to set up your themes agian. +* Now confirming pending transactions update date of transaction to the + date of confirmation. You can disable this in **Preferences** > **Pending transactions** ### Fixes and enhancements From 5e2d8001b464847f420c84ae8342e0a74fcea8b3 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Mon, 25 Nov 2024 01:19:40 +0800 Subject: [PATCH 12/19] other fixes, changes --- assets/l10n/mn_MN.json | 4 +-- lib/routes.dart | 4 +-- .../preferences/pending_transactions.dart | 21 ++++++++------- .../preferences/privacy_preferences_page.dart | 19 +++++++------- lib/routes/preferences_page.dart | 26 ++----------------- 5 files changed, 27 insertions(+), 47 deletions(-) diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index be3fb54..4cdfc31 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -187,11 +187,11 @@ "preferences.pendingTransactions": "Хүлээгдэж буй гүйлгээнүүд", "preferences.pendingTransactions.requireConfirmation": "Баталгаажуулалт шаардах", "preferences.pendingTransactions.requireConfirmation.description": "Баталгаажуулаагүй гүйлгээнүүд орлого, зарлага, дансны үлдэгдэлд тооцогдохгүй", - "preferences.pendingTransactions.updateDateUponConfirmation": "Гүйлгээ баталгаажуулах үед огноо шинэчлэх", + "preferences.pendingTransactions.updateDateUponConfirmation": "Гүйлгээ баталгаажуулахад огноо шинэчлэх", "preferences.pendingTransactions.updateDateUponConfirmation.description": "Идэвхгүй болгож анхны огноог ашиглах боломжтой", "preferences.pendingTransactions.homeTimeframe": "Нүүр хуудсанд харуулах", "preferences.privacyMode": "Нууцлалын горим", - "preferences.privacyMode.description": "Дүнг одолж харуулна. Нүүр хуудасны баруун дээр байрлах нүдэн дээр дарж асаах/унтраах боломжтой", + "preferences.privacyMode.description": "Дүнг нууж харуулна. Нүүр хуудасны баруун дээр байрлах нүдэн дээр дарж асаах/унтраах боломжтой", "preferences.privacyMode.enableAtStartup": "Эхлэх үед идэвхжүүлэх", "preferences.moneyFormatting": "Мөнгөний хэлбэр", "preferences.moneyFormatting.preferFull": "Бүтэн дүнг харуулах", diff --git a/lib/routes.dart b/lib/routes.dart index a73fd50..373e58f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -169,8 +169,8 @@ final router = GoRouter( builder: (context, state) => const ThemePreferencesPage(), ), GoRoute( - path: "startupPrivacy", - builder: (context, state) => const StartupPrivacyPreferencesPage(), + path: "privacy", + builder: (context, state) => const PrivacyPreferencesPage(), ), GoRoute( path: "moneyFormatting", diff --git a/lib/routes/preferences/pending_transactions.dart b/lib/routes/preferences/pending_transactions.dart index 883d73e..b9a1154 100644 --- a/lib/routes/preferences/pending_transactions.dart +++ b/lib/routes/preferences/pending_transactions.dart @@ -33,6 +33,16 @@ class _PendingTransactionPreferencesPageState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: InfoText( + child: Text( + "preferences.pendingTransactions.requireConfirmation.description" + .t(context), + ), + ), + ), const SizedBox(height: 16.0), ListHeader( "preferences.pendingTransactions.homeTimeframe".t(context), @@ -60,6 +70,7 @@ class _PendingTransactionPreferencesPageState .toList(), ), ), + const SizedBox(height: 16.0), CheckboxListTile.adaptive( title: Text( "preferences.pendingTransactions.requireConfirmation" @@ -69,16 +80,6 @@ class _PendingTransactionPreferencesPageState onChanged: updateRequirePendingTransactionConfrimation, ), const SizedBox(height: 16.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: InfoText( - child: Text( - "preferences.pendingTransactions.requireConfirmation.description" - .t(context), - ), - ), - ), - const SizedBox(height: 16.0), if (requirePendingTransactionConfrimation) ...[ CheckboxListTile.adaptive( title: Text( diff --git a/lib/routes/preferences/privacy_preferences_page.dart b/lib/routes/preferences/privacy_preferences_page.dart index 3098bb7..efeb522 100644 --- a/lib/routes/preferences/privacy_preferences_page.dart +++ b/lib/routes/preferences/privacy_preferences_page.dart @@ -3,16 +3,14 @@ import "package:flow/prefs.dart"; import "package:flow/widgets/general/info_text.dart"; import "package:flutter/material.dart"; -class StartupPrivacyPreferencesPage extends StatefulWidget { - const StartupPrivacyPreferencesPage({super.key}); +class PrivacyPreferencesPage extends StatefulWidget { + const PrivacyPreferencesPage({super.key}); @override - State createState() => - _StartupPrivacyPreferencesPageState(); + State createState() => _PrivacyPreferencesPageState(); } -class _StartupPrivacyPreferencesPageState - extends State { +class _PrivacyPreferencesPageState extends State { @override Widget build(BuildContext context) { final bool privacyMode = LocalPreferences().privacyMode.get(); @@ -27,9 +25,12 @@ class _StartupPrivacyPreferencesPageState crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16.0), - InfoText( - child: Text( - "preferences.privacyMode.description".t(context), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: InfoText( + child: Text( + "preferences.privacyMode.description".t(context), + ), ), ), const SizedBox(height: 16.0), diff --git a/lib/routes/preferences_page.dart b/lib/routes/preferences_page.dart index dec040c..f683dcd 100644 --- a/lib/routes/preferences_page.dart +++ b/lib/routes/preferences_page.dart @@ -28,16 +28,11 @@ class _PreferencesPageState extends State { final FlowColorScheme currentTheme = getTheme(LocalPreferences().themeName.get()); - final int pendingTransactionsHomeTimeframe = - LocalPreferences().pendingTransactionsHomeTimeframe.get() ?? - LocalPreferences.pendingTransactionsHomeTimeframeDefault; - final bool enableGeo = LocalPreferences().enableGeo.get(); final bool autoAttachTransactionGeo = LocalPreferences().autoAttachTransactionGeo.get(); final bool requirePendingTransactionConfrimation = LocalPreferences().requirePendingTransactionConfrimation.get(); - final bool startupPrivacy = LocalPreferences().privacyMode.get(); return Scaffold( appBar: AppBar( @@ -45,16 +40,6 @@ class _PreferencesPageState extends State { ), body: SafeArea( child: ListView(children: [ - ListTile( - title: Text("preferences.home.pending".t(context)), - subtitle: Text( - "general.nextNDays".t(context, pendingTransactionsHomeTimeframe), - ), - leading: const Icon(Symbols.hourglass_top_rounded), - onTap: () => pushAndRefreshAfter("/preferences/home"), - // subtitle: Text(FlowLocalizations.of(context).locale.endonym), - trailing: const Icon(Symbols.chevron_right_rounded), - ), ListTile( title: Text("preferences.pendingTransactions".t(context)), subtitle: Text( @@ -146,16 +131,9 @@ class _PreferencesPageState extends State { trailing: const Icon(Symbols.chevron_right_rounded), ), ListTile( - title: Text("preferences.startupPrivacyMode".t(context)), + title: Text("preferences.privacyMode".t(context)), leading: const Icon(Symbols.password_rounded), - onTap: () => pushAndRefreshAfter("/preferences/startupPrivacy"), - subtitle: Text( - startupPrivacy - ? "general.enabled".t(context) - : "general.disabled".t(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + onTap: () => pushAndRefreshAfter("/preferences/privacy"), trailing: const Icon(Symbols.chevron_right_rounded), ), ]), From ef029c03fec8672143b3dd2c607927f4de48aca7 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Mon, 25 Nov 2024 01:21:42 +0800 Subject: [PATCH 13/19] =?UTF-8?q?l10n:=20trim=20=E2=9C=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/l10n/en_IN.json | 2 +- assets/l10n/en_US.json | 2 +- assets/l10n/mn_MN.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/l10n/en_IN.json b/assets/l10n/en_IN.json index 398916d..f1635dd 100644 --- a/assets/l10n/en_IN.json +++ b/assets/l10n/en_IN.json @@ -188,7 +188,7 @@ "preferences.pendingTransactions.homeTimeframe": "Show on home", "preferences.pendingTransactions.requireConfirmation": "Require confirmation", "preferences.pendingTransactions.requireConfirmation.description": "Pending transactions will not be counted towards income, expenses, and account balance", - "preferences.pendingTransactions.updateDateUponConfirmation": "Update date upon confirmation", + "preferences.pendingTransactions.updateDateUponConfirmation": "Update date on confirm", "preferences.pendingTransactions.updateDateUponConfirmation.description": "Disable to retain original transaction date", "preferences.privacyMode": "Privacy mode", "preferences.privacyMode.description": "Nasconde importi. Attiva/disattiva con l'icona dell'occhio nell'angolo in alto a destra della schermata iniziale", diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index d656e6d..e5fbbb3 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -188,7 +188,7 @@ "preferences.pendingTransactions.homeTimeframe": "Show on home", "preferences.pendingTransactions.requireConfirmation": "Require confirmation", "preferences.pendingTransactions.requireConfirmation.description": "Pending transactions will not be counted towards income, expenses, and account balance", - "preferences.pendingTransactions.updateDateUponConfirmation": "Update date upon confirmation", + "preferences.pendingTransactions.updateDateUponConfirmation": "Update date on confirm", "preferences.pendingTransactions.updateDateUponConfirmation.description": "Disable to retain original transaction date", "preferences.privacyMode": "Privacy mode", "preferences.privacyMode.description": "Obscures amounts. Toggle with the eye icon at the upper right corner of home screen", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index 4cdfc31..0a2a123 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -187,7 +187,7 @@ "preferences.pendingTransactions": "Хүлээгдэж буй гүйлгээнүүд", "preferences.pendingTransactions.requireConfirmation": "Баталгаажуулалт шаардах", "preferences.pendingTransactions.requireConfirmation.description": "Баталгаажуулаагүй гүйлгээнүүд орлого, зарлага, дансны үлдэгдэлд тооцогдохгүй", - "preferences.pendingTransactions.updateDateUponConfirmation": "Гүйлгээ баталгаажуулахад огноо шинэчлэх", + "preferences.pendingTransactions.updateDateUponConfirmation": "Баталгаажуулахад огноо шинэчлэх", "preferences.pendingTransactions.updateDateUponConfirmation.description": "Идэвхгүй болгож анхны огноог ашиглах боломжтой", "preferences.pendingTransactions.homeTimeframe": "Нүүр хуудсанд харуулах", "preferences.privacyMode": "Нууцлалын горим", From 69153aaee41d0b0adad5c3182dbe7213d574b7c1 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Mon, 25 Nov 2024 01:21:53 +0800 Subject: [PATCH 14/19] bump build number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index c490f8e..cfc7e79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.9.0+82" +version: "0.9.0+83" environment: sdk: ">=3.5.0 <4.0.0" From 46c54b7681d4889d2556cf91b24661a8fe1246fb Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Tue, 26 Nov 2024 22:17:43 +0800 Subject: [PATCH 15/19] chore: cleanup --- .idx/dev.nix | 48 ------- .../metadata/en-US/full_description.txt | 10 -- fastlane/android/metadata/en-US/icon.png | Bin 14054 -> 0 bytes .../metadata/en-US/short_description.txt | 1 - fastlane/android/metadata/en-US/title.txt | 1 - old_theme_script.py | 130 ------------------ 6 files changed, 190 deletions(-) delete mode 100644 .idx/dev.nix delete mode 100644 fastlane/android/metadata/en-US/full_description.txt delete mode 100644 fastlane/android/metadata/en-US/icon.png delete mode 100644 fastlane/android/metadata/en-US/short_description.txt delete mode 100644 fastlane/android/metadata/en-US/title.txt delete mode 100644 old_theme_script.py diff --git a/.idx/dev.nix b/.idx/dev.nix deleted file mode 100644 index 5083f1a..0000000 --- a/.idx/dev.nix +++ /dev/null @@ -1,48 +0,0 @@ -# To learn more about how to use Nix to configure your environment -# see: https://developers.google.com/idx/guides/customize-idx-env -{ pkgs, ... }: { - # Which nixpkgs channel to use. - channel = "stable-24.05"; # or "unstable" - - # Use https://search.nixos.org/packages to find packages - packages = [ - pkgs.flutter - pkgs.cmake - pkgs.android-tools - ]; - - # Sets environment variables in the workspace - env = {}; - idx = { - # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" - extensions = [ - "circlecodesolution.ccs-flutter-color" - "Dart-Code.dart-code" - "Dart-Code.flutter" - "jeroen-meijer.pubspec-assist" - ]; - - # Enable previews - previews = { - enable = true; - previews = { - android = { - manager = "flutter"; - }; - ios = { - manager = "flutter"; - }; - }; - }; - - # Workspace lifecycle hooks - workspace = { - # Runs when a workspace is first created - onCreate = { - install-deps = "flutter pub get"; - }; - onStart = { - }; - }; - }; -} diff --git a/fastlane/android/metadata/en-US/full_description.txt b/fastlane/android/metadata/en-US/full_description.txt deleted file mode 100644 index 36cbd3d..0000000 --- a/fastlane/android/metadata/en-US/full_description.txt +++ /dev/null @@ -1,10 +0,0 @@ -Flow is a free, open-source, cross-platform personal finance tracking app. - -## Features - -- Multiple accounts -- Multiple currencies -- Fully-offline -- Full export/backup - - JSON for backup - - CSV for external software use (i.e., Google Sheets) diff --git a/fastlane/android/metadata/en-US/icon.png b/fastlane/android/metadata/en-US/icon.png deleted file mode 100644 index f196e7e6c96f65cd1ab3dfc4892a9dc22ead37c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14054 zcmeHtXH-*LyY5UvF9t=5AP~zI*&?7)q$b!aQBlAG#Ev4MA|N1Sp(6MpMz`C7h+qW; z0R>b*OsuGY#4SZ>0a3uvA+(TW-FL-(?zs2IH_oqbj5EgBj6ssMW}owI&-2dH>)e)W zsu`#ugfv&KSh4{j1^B6eRFvS0Z8YZue5oE>u{jta^VP&Z>5lWId+?GJykYqwl>Kq) zPon-_*R`$)J>;lMwkslJ8@Y0c>&By`?ndUB>Y)<6pR-mmxPT&=RH}GFbz7F&)9ulx z-PZ&io_Fq#S!=b|)CES&NGPgGTwVOsB4k=wlVV#_Vu|Y7a?5$PSN3n15w^y{T(~ep zYvnF4D|76gt%mt%N@dKa?Ym^DcuwgI`y^65-)i{#vFoXo?b4E2e=ls7P8iQ-&#esCh8vWo;hlva?DWmvCGqD`iz+fm4^ws#jzv%9^{4CZx?B%5m!w~IE=pYk zMdH&SmxX+<4&Twy4fbK*b_B~?6p$_++QA+iWY1?1%PYv)mJwdWT11($IXy<4C!6e#cg!pl8cDmA`r`%Y2 z?v_`@kF9U<`Yt3V3o4}s*b5j*bpGf2IX_N#G9#~<@Hg0?1BQkc_1BFpIA@)wt00Zp zB$P(2N|9YiG{PeqOwt;K_ptNJw2pT1RxHp!R{9DEGxiJ{7es|Wv;Sd|_PNbXxy%09 zVhz+0OhydOqfV;mssUxVV^fek&R$7%Zv9COVsY*i5OJLV-MhI}JCsPk| z%|GO~c#88t>!X~BWYpmTlUVKceBP&H#_GmHQ;IP#^~l9Mc=zCLV#(js9$Cfr*r6XtRqL{!@56wP%eQx-;i?Ktxs0T{ zUSNWMiNC$TT`(EdLEz+f%b~Bg;dFu`_NMv`&X!3k_uNML#4Kl-3a(gno3LW{>tvZ%_4@V_;p4+8H59X1118~MV z_lwsc(e(>KYZj&`=8}nORWH~*No0}5d0kdG@vJXV=GX7Pt+StW31ry(R*%eaiyD9Y z8X0+AMJQLJ7)R0@Z|e*KNKfrx=Bgl8;sihS+=ax0%L=kcFC8IW&`|T2J#+?pQV^*J z^xf-qI@`e)bl`Na5aX}o zoQ;lX=x1ZmVZ$Z&$h;}xEVbq`fnB``%Xq;L05U=;hxvAWL^L) z#wlb}Ao_lh%wzQ}(ZhN{k_Yu=%2r-N8lx7WKp3%Y-aG?V-?0cu85u*U??KUM4ih0( z?zKT{gv}zM=Ch~iU#F4vmwdk9=}keiq0=W*ab^8AL;KKW&7+DSeD@us0+`>6kQy5> zo+z!zD*7I>dU_3lX@stJX^=E*ITRw4zIwMC9-PtU7BZM|W7Bb3^Fc z*NNnJ%~E5+iuO0)%J2HBON~d=c4;H*=-HFxU42hIX(ax z3^*lh7u3uI^MRSy?u98IRz(bR6EbPwsAvu`z>aVQ8bT`-5bY8PL*dUxDDR2iJJS%F zIT2xAvs@|Y99X6u6$TIyFtniO9|VY!w z3ejzFGAKSb;J{Nf6Al%{TYaC1F+d^_1^|xyQvu#hKVTSn`&Y64(GW40gfSe53Nd^8 zsWd3pVztMdClg13Sj|&@g}PSFr7s@E`3LB zZ)azWqo6h?7*8UMm#vT8Re^7^`0ki=O>5`S+uE`Vo(8@_mwqnryo%dnKK)-@M{p9H z0gIx`Q2s6)UarzEZodmSd|sUjNp3^WYA)voN*r!B{)>a>;VzAeoOQ(tDd;=9x%4 z*wVVs6?Md?C_mPfi@g$~M6iYPw8lI8PH0>4Jy~y^v|+d{bCLq)_GPf)tXz8-srQ$Q zJKb=6L!I}(mM41)RA5%SEO{Ik&V3pq48$baz|9id|ddQ-J6LL zihp5c8o#q%@O$Gry({CsQwll-dlg6>QQYx9y|xTw{z8>-km|As>jJmYbbR47RPvxl zwiSF|g({+Xm@-in^$u6^idoi|Jva8nOclmR)yNlL?obwCGeM{8vMj`%-vr8^U$mV> zN0>%;h0b7bLE0WV93u0g*ljQ&V@K+lKYeATOm$QSdhgXe0ge0AwI14NzDf%l$~-hA zXtLim0xohJ3HebH*q2&Ape$eK?YsM_xrd3a>O#o8Z>b(y3aou0Li|Q;sT!ICV?V5$ zh|FdC^jy*NokBXV4iq)ZLm4Ho9ad`5zV^CEV&Bkt23JUkPu9hcT%AI`Ew1$(-Xfu7 zo!2I+rl2p8-}@hN`(q|YoTl+;1JUY)pVZ3KA+ZS0Umq6jkyfip78M6Dsh54F{a?vM z_{+!|QbLO}I`$q%(%a7~W%Z76AKu;=S4aNDoNBa@Fpe+YUiB^2b(#u4^R)+7GfzQ_ zr77gmO_oiWc}nfC1c)HEl)?ViY+)k(MN6lyw>rvr*QJdc7{vBM?#rOF$;PEH}`AAdtn#{(NBG6{(3x$pB4SM`rUpG3 zjnfU@joA-HY>LYN9d);Z8T0?m6g4c5?a@+D@@?$Kf0)Sb5D_UlatiFx>Z5lb&VfTp zX+Ozh)enmf+Ka?jMm(_z5NpO4G^nBzcv_e$S=#CA`*rIwj*}1SaDpw;?LXsqkDqr< ztAjqYcNoH}eEV(_tmh!ns7e(T<9vZaOw)!qN%eL%I&v8X8exr?h?BL+B`>_$SkpIR zUJtcVD89WJaFcR;K z`$T*&Vq-3?3W#S!?X5e|Pt98KO*N*lQI?{-{KLUk=_E`B5e&FL5Gd`B;cwt&)4VUKkuRI(5XN-X z6Adt*XRMnjoeyQD69N~MQ&||))Qp_aI~%1uvQjDk*kBHR+hAVvs=~T>%8>uy$QFGi zVd~hs3&~|1Ft--NabW>w$p(V0eU$Iuq5sv2Gs3Eu9*r_7s*U`#ZE6O&q^npnGDPu;pTfm zw7?;1fUk-7=*8(QvaOsB)ZTHj%D-`7Y7uu#diT6?8NWt3ncHMNu&%Z9)cj}uWl!*_2&?|vK_QQ{ z|H};oMAMza?~E0=??+FK$Of*>?<;oKf0^+Th+Z|&frigr2sNn zHi4gwxYpZxLA|yc9Ok?To-d<5y zGK730M+kUOMfWgsDqZlYRCI^vH+njVpH``)mSXZjoXw*nmg@ob zTEhb~#c|%ICiYOiql~fys@JWAf(4-7ACwaT>By!mfl8Z**knCtw6=7tx>dGI8^$OA zy%sYXyV)Z@4wXtb{|R$32NU11Jyg1x3Y}kpc)w$PsC1t)JV5x1oHHhj<#jUC<3YQ? z@^wrLdBW`xC-*U*oF*fyqcDPT%R73B+hl~v0HKI5zv>Ch)X3b3eNRiun9sqk$WF}(^CN`aX7$dxyAbE4B>I2Q-M zh=En%f(32isPngVw8Q>BY1BU%hJQGzBo6euMU5BSg^-L6+gIk7u>@)z6T6@%mA zNW`bYGIhtBt3Us!dRVD`?#43g{KJ-kFEQtrP1d(S{7joq)_9j z``%(=9TagIdY#_xw&vm!-_cyOF?b%ZBBmeuvMUr(5iv>9VjR0uLmKz7P0=j}YE%x?e0a<+x48LZ4c>G(Pj$xOfK2{{zCeRYYwM!A_ys45mIQ z?INsw)0OMb{(6?F&Q~3pcMDj8?S(GEd59MQTb;R%9#1*)SE0814{o$9`@(XR1uB)h zXvig-+RU&!+BF+JfJtUyFW8JH6-%+7?=uxq%_3M_%<0zl?q?)#PeGgv{4gsK2HM@w zB(*zj*rA!X!0zfq#-9qT0^;qlUHw$i6$4T^R3l#eRzh)(EkkV#n#pWVyhAntm0FSpv zMNd23(0c_43ykK`-h3U=peU>W%g}Fe%pBah&;~1Wb0)n8Huh*XAmeQmImZ126Z)&l zAE8}P;CJ>Vd7qQ!FQ8s*JG7RxqE)EP00JW3HtKC0i^hTIE(u1s>TN|kpPnjjlX#QS*#>+9 z(PQ3qGCDcvqH=C2gds_orabfjH~R0g1B(I-!gJ+83sNcA4#5m94r*%&V+dGaAMJ(o zj~W`94A=!kWgIXVOCPv(KJKcCIId@^w6SV*Xa}I{KRXj^%d6}XbL`3l>CXeBQ(S`Y z^j8Mzrm=rf$1qbMreDecn_mvt$+6~|G!^`)e-k~Y8d-tI+!pxE!iMV;aEg>hcWNG` zGeCIKDyti%5VHMAn7EFVrR;QBLqH-u?|EDK(wIEcyC14X^J&Prt7X|#1!5=oal+~B z$`&KRRxGUka!f-jIi#WcJTM=LwXuhXOA-`2oIDY6 zn#lRSg=C~ev{BGCoU?4u`qgp$zUt9ChkHuQfDoRL!9;y34C2TboUeed!|y)q>#efl z1}2+O?i)~qTCVd@-QLlaz~7yQsTr>ilTb@sbo2Ur%Ym`3@U&s?VWdn$`OBhqsGxAD zVPW|FmaNkTKTY0&^3RSKVr!u5%T>2}$9<07w{PbBI8EXk49VEyl`{xZhN0Kxlv^-0 zLTVc2T+L0^eTx+)k#->SpyXB@J?u$#W_Nm!R7B$8C#%w{6#<+^ZwNJR*y2|LMrrrA z(eQcbQeS=8go`cS$?C-TG>>!&HjSOKiNc7w$D>MvRYSDvFd(Js)7oh09d3#DSDT*fb+Do<4fuzQH*AySl)|sqrW^$7QGRIRlomGHNc6wXXyFt5Vo9vb zqqU;pJys|`w6<2o&YT+pr=2Qh2vo;!$_1J7XNms03g@*9jWpkK?+_Bi{Ar5jvf!%H zj0)#f0kU$;VKIvMWSqc8J%nGp*ORqbviH7wb-Qu@Y$bkzu!lQhirPRT$uylZ5mHZUGF89iA>a5##(Tl6Vydz9iyNB#g&F#kRe1Dx)plpCEZ8`@fF%YC_O zEz`$LyRtTbcOx3DrR#9!MVm#_V z;_O95ohwm8g%0?~Uj#y4;E?ED(+rNCZ}7Y{ADxU~3d>o@-CFtVdk-B60Cr?8YTRTP zVE!fTBFM|wVukGm!e)By1@u%8d|0edSt#Y2%;mcUpQsnE)?gLvUowA`ioF4*;`Nqp z4NMR-9oXTPAi;k5dXu1G!g zs-^6x`KRB|rt?tC$w?>^+?4stX1da8eJd~6WHU$s%|W>a;6k0NV)5&*9Mv># z#?E5}?!ol=CJ_61203p9cmMyYlCE@b$lid!41?PckZhQBHdT)j@uhnwe#>}7nRk`& z0B^{QMk(GH$DMsHO9>M%i7`-s)S}rDU!&3Xk95@8q6Qv2vP-gf!O3dHy-^i!u!v1* zA2rhkg=;2#mXW+Hfg|=xNI@CJ_$mBFNwvNeJD%3Pi6gF^foIPo@zp@a%k#$jLKE0t zi=_B+?=v-*^&xrEE3OT4?sQhgE&C^l0b;Lr~Bimw>S~7g$(O`28837}M8XmUyoh4`adcU?fL-I}Z z-S6U)E4vz5AI|azMupiWrtobE0s%jE@HTp2Ls!}u%+%JRJa=a@A<)YT2V_K`hS?$Q z0k@r4wVHK-duQZ#Khv&ntJKI?pzv4K!udd4?iRgk4V98QR6qe+<^jucD5Nwpu-4H` z$!nCtp9_}mki?{D?lk#h01+2hI84RknjTsyl~-tV)avJU|7>Y9yVY3Po!h6J2B9*| zrv#^p?iT9j;vXqYFb@nSM>d+afxXum9L<$j6<6A3oxKZuv zkD5dBMzF)EdWWHccBGXP!3NpFc zvC7Kong8nWr~MHk(8CcMz=(YMuV8E>Buq|;G@QP-G;GlEN2u19ZNpQW);~_vgpMNs zr}0}YtV2iW($ANF9N76ukZmLzx^Jog;MR;|q`xm3@cjr8iDmSNQvJI`sX%&ET8Q=B zbCjyINtMq=|MUkrm1pXKH6ZfVSACUTHA&+J(|9^S-1UHnM)1<+O7!lVlHhVddRKVh zaxgQFM7U)jip3W#0!1K3@Lp#+${im#TKhoWBgF%i)eCc;h+%=84+_dnix@4f(TDjp@EXylfm)Ky#kJwS&Qx zq+5_#f#S1JtYq>mqV7B%a-TvTDH+`esY9P6DUpXA}(jyrorH5erE+9Nyb6h)^>7b;BgrO*f;vy;mQxq|jwZu2uI` zb}hs=z;0xnSKQY==BSLk2wq83n9o-K)N{q~@zIk01;E0B9l>leIO4p~-s=tr`YSCt zaK0Yks=V$%?)%SRQw!aqPLP5DS!O!0vvY_ZU;f;gdila~x|!(I=hEl1iB&^=;8*~= zzr&a7;?C`Wor}*Bu?2V;wdxz}Jf+H^YhRNLD^e40V8HXk@p?2RJ*>14q4G`UW@p|y)gD_09Bh1q&r1Z=G_Z)gg znUiXUGIHjDc2N^SO9Hxgz&tfxN_~iosftp9vQCy z#hk)5b76hY`&&8SOXX1ik@_`%zvNNMuQ|bL>%NPW zkrs5aBS|6EA2N7w7R%sivqD>4hxqTJYHeh!d^z1h2JwT19M00}?w*fR+YCuL`H2M7 ze|{BfPAwLber+SQUi0i=MhE5B6Wqhv@NUQGj9UIu~KH zQxu=7jCiZS1!anIxPq<5j!z5t-j37*0gypOx$|Le29mCVhmf;aeZx_B9$%b;--d1h zfy0HT8yJxpZ*=ZGE)EN%%|JYYAMv+-qNPJ}C>7ZMx+~eqa;>wMWs^|Na+s5;`sd-n zFb$L&4R%D`EA27U=y6A{E_i~!BfhlX2r`Jyb@KUm{i&B1SDe9h=oawk2pa;SsB8Xi zs10|qm}>q{M&7mV2LGx4_m!{rKX>~6$vG=~Kx7BAwl#JO@DF}p;ras%)Cmg=uwF2< zcC0l>_Ltkp)OFL=bUE9UkQ2#0sOyM+Xv_A7VM_ zWi$XBsKW?WLWKasrAV7B?%v2S>aZhVP5X=Ua6I5_)9hfG&OPA%4nOX{PoeDH8+gfQ zqn;EZ9JAuo`t46l;I;*vP}FUoiB}fNb{9H@7pzGw+)#AStxxdPOGK|}_$m0dJ{md! zkJ{lrw4VpT+`y(8YTE*jJ#bC)HdPRQi~d{__hH4(Fs^YY`$*SBP9FubH6RV>o@?4N z%s?UAmiz(;Jd0XGZZ|Ln`9qV9bE2 z?K?r(New4Z)PGMhA{s^%|3`9p|Gk-pf&U%*`On1uIhTLL@|Pyl2vK05e-!yYV=iR$ Z-;SLMU%2r&ARAy*mbxv;Uc@^7e*o>_TI2u# diff --git a/fastlane/android/metadata/en-US/short_description.txt b/fastlane/android/metadata/en-US/short_description.txt deleted file mode 100644 index bb5da9a..0000000 --- a/fastlane/android/metadata/en-US/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -A free and open-source personal finance tracker \ No newline at end of file diff --git a/fastlane/android/metadata/en-US/title.txt b/fastlane/android/metadata/en-US/title.txt deleted file mode 100644 index 06773b6..0000000 --- a/fastlane/android/metadata/en-US/title.txt +++ /dev/null @@ -1 +0,0 @@ -Flow \ No newline at end of file diff --git a/old_theme_script.py b/old_theme_script.py deleted file mode 100644 index 3ebd3cd..0000000 --- a/old_theme_script.py +++ /dev/null @@ -1,130 +0,0 @@ -import colorsys - -import requests - -import math - - -def calculate_luminace(normalized_value): - index = normalized_value - - if index < 0.03928: - return index / 12.92 - else: - return ((index + 0.055) / 1.055) ** 2.4 - - -def calculate_relative_luminance(r, g, b): - return 0.2126 * calculate_luminace(r) + 0.7152 * calculate_luminace(g) + 0.0722 * calculate_luminace(b) - - -def calculate_contrast_ratio(color: tuple[float, float, float], otherColor: tuple[float, float, float]): - l1 = calculate_relative_luminance(*color) - l2 = calculate_relative_luminance(*otherColor) - return (l1 + 0.05) / (l2 + 0.05) if l1 > l2 else (l2 + 0.05) / (l1 + 0.05) - - -def rad(degrees: float): - return degrees / 180 * math.pi - - -def rgbToHex(r: float, g: float, b: float): - rs = hex(math.floor(r * 255))[2:].zfill(2) - gs = hex(math.floor(g * 255))[2:].zfill(2) - bs = hex(math.floor(b * 255))[2:].zfill(2) - - return rs + gs + bs - - -type RGBColor = tuple[float, float, float] - - -class FlowColorScheme: - def __init__(self, surfaceColor: RGBColor, onSurfaceColor: RGBColor, primaryColor: RGBColor, onPrimaryColor: RGBColor, accentColor: RGBColor, onAccentColor: RGBColor, contrast: float, name="undefined"): - self.name = name - self.surfaceColor = surfaceColor - self.onSurfaceColor = onSurfaceColor - self.primaryColor = primaryColor - self.onPrimaryColor = onPrimaryColor - self.accentColor = accentColor - self.onAccentColor = onAccentColor - self.contrast = contrast - - def setName(self, name): - self.name = name - - def toDict(self): - return { - "surfaceColor": self.surfaceColor, - "onSurfaceColor": self.onSurfaceColor, - "accentColor": self.accentColor, - "onAccentColor": self.onAccentColor, - "primaryColor": self.primaryColor, - "onPrimaryColor": self.onPrimaryColor, - "contrast": self.contrast, - } - - def toDart(self): - return f"""final {self.name} = FlowColorScheme( - isDark: false, - surface: const Color(0xff{rgbToHex(*self.surfaceColor)}), - onSurface: const Color(0xff{rgbToHex(*self.onSurfaceColor)}), - primary: const Color(0xff{rgbToHex(*self.primaryColor)}), - onPrimary: const Color(0xff{rgbToHex(*self.onPrimaryColor)}), - secondary: const Color(0xff{rgbToHex(*self.accentColor)}), - onSecondary: const Color(0xff{rgbToHex(*self.onAccentColor)}), - customColors: FlowCustomColors( - income: Color(0xFF32CC70), - expense: Color(0xFFFF4040), - semi: Color(0xFF6A666D), - ), -); // contrast: {self.contrast}\n""" - - -def to_camel_case(value): - content = "".join(value.title().split()) - return content[0].lower() + content[1:] - - -def generateColors(backgroundColor: tuple[float, float, float], targetContrast: float, hueOffset=0, numberOfColors=16, saturation=0.2, initialBrightness=1.0, brightnessStep=-.05, totalTrials=5): - colorContrastList: list[tuple[tuple[float, float, float], float]] = list() - - unitHue = 1 / numberOfColors - - for i in range(numberOfColors): - hue = (hueOffset + unitHue * i) % 1.0 - - r, g, b = colorsys.hsv_to_rgb(hue, saturation, initialBrightness) - - trials = totalTrials - contrast = calculate_contrast_ratio(backgroundColor, (r, g, b)) - - while contrast < targetContrast and trials > 0: - trials -= 1 - r, g, b = colorsys.hsv_to_rgb( - hue, saturation, initialBrightness + (brightnessStep * (totalTrials - trials))) - contrast = calculate_contrast_ratio(backgroundColor, (r, g, b)) - - colorContrastList.append(((r, g, b), contrast)) - - return colorContrastList - - -def generateDefaultColors(light=True): - light_bg = 0xf5 / 255.0, 0xf6 / 255.0, 0xfa / 255.0 - dark_bg = 0x11 / 255.0, 0x11 / 255.0, 0x11 / 255.0 - - bg = light_bg if light else dark_bg - - generated = list() - - for [color, contrast] in generateColors(bg, 0.0, saturation=1.0, initialBrightness=0.31, totalTrials=0, brightnessStep=0.033, hueOffset=0.776): - generated.append(color) - - return generated - - -default_darks = generateDefaultColors(light=False) - -for d in default_darks: - print(f"${rgbToHex(*d)}") From 9da698f234f3087e3be939369c95f3bd9f9cd6fd Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Thu, 28 Nov 2024 00:57:12 +0800 Subject: [PATCH 16/19] Iteration 2 --- CHANGELOG.md | 11 +- assets/l10n/en_IN.json | 5 +- assets/l10n/en_US.json | 5 +- assets/l10n/it_IT.json | 5 +- assets/l10n/mn_MN.json | 7 +- lib/prefs.dart | 8 +- lib/routes.dart | 5 + .../new_transaction/description_section.dart | 39 +--- .../new_transaction/input_amount_sheet.dart | 4 +- lib/routes/new_transaction/title_input.dart | 1 + .../preferences/haptics_preferences_page.dart | 48 +++++ .../preferences/numpad_preferences_page.dart | 17 -- lib/routes/preferences_page.dart | 15 ++ lib/routes/utils/edit_markdown_page.dart | 199 ++++++++++++++++-- lib/utils/extensions/toast.dart | 5 +- lib/utils/numpad_haptic.dart | 5 - lib/utils/utils.dart | 1 - lib/widgets/general/markdown_view.dart | 172 +++++++++++++++ lib/widgets/general/money_text.dart | 6 + lib/widgets/numpad_button.dart | 6 +- lib/widgets/transactions_date_header.dart | 29 ++- lib/widgets/utils/utils.dart | 5 - pubspec.yaml | 2 +- 23 files changed, 496 insertions(+), 104 deletions(-) create mode 100644 lib/routes/preferences/haptics_preferences_page.dart delete mode 100644 lib/utils/numpad_haptic.dart create mode 100644 lib/widgets/general/markdown_view.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e9c66..043057f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,22 +4,27 @@ ### New features -* Title flow now converts all currencies into primary +* Home screen day title flow now converts all currencies into primary +* Markdown editor (transaction description) now has a preview, and minimal toolbar ### Changes * Renamed upcoming/planned -> Pending transactions -* Home tab pending transactions time range settings has been revised. +* Home screen pending transactions time range settings has been revised. You will need to update your preferences again. * Deprecated old theme system (light/dark). If you missed an update since this features was introduced, you will need to set up your themes agian. * Now confirming pending transactions update date of transaction to the - date of confirmation. You can disable this in **Preferences** > **Pending transactions** + date of confirmation. You can disable this in **Preferences** > + **Pending transactions** +* Haptic feedbacks setting has been moved, and now controls all haptics. + (Including error feedbacks) ### Fixes and enhancements * Transaction list tile title color is now fixed in light themes * Wavy divider color now follows the theme change +* Disabled Autocorrect on transaction title, so it no longer alters suggestions ## Beta 0.8.1 diff --git a/assets/l10n/en_IN.json b/assets/l10n/en_IN.json index f1635dd..3740fd4 100644 --- a/assets/l10n/en_IN.json +++ b/assets/l10n/en_IN.json @@ -110,6 +110,7 @@ "transaction.description": "Notes", "transaction.description.add": "Add notes", "transaction.description.markdownSupported": "Markdown supported", + "transaction.description.preview": "Preview", "transaction.location": "Location", "transaction.location.add": "Add a location", "transaction.location.edit": "Tap on the map to edit", @@ -164,8 +165,8 @@ "preferences.numpad.layout": "Numpad layout", "preferences.numpad.layout.classic": "Classic", "preferences.numpad.layout.modern": "Modern", - "preferences.numpad.haptics": "Button feedback", - "preferences.numpad.haptics.description": "Sound/haptic feedback upon click", + "preferences.hapticFeedback": "Button feedback", + "preferences.hapticFeedback.description": "Sound/haptic feedback upon click", "preferences.transactionButtonOrder": "Button placement", "preferences.transactionButtonOrder.description": "Change new transaction button placement", "preferences.transactionButtonOrder.guide": "Drag the buttons to reorder", diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index e5fbbb3..6f38c5f 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -110,6 +110,7 @@ "transaction.description": "Notes", "transaction.description.add": "Add notes", "transaction.description.markdownSupported": "Markdown supported", + "transaction.description.preview": "Preview", "transaction.location": "Location", "transaction.location.add": "Add a location", "transaction.location.edit": "Tap on the map to edit", @@ -164,8 +165,8 @@ "preferences.numpad.layout": "Numpad layout", "preferences.numpad.layout.classic": "Classic", "preferences.numpad.layout.modern": "Modern", - "preferences.numpad.haptics": "Button feedback", - "preferences.numpad.haptics.description": "Sound/haptic feedback upon click", + "preferences.hapticFeedback": "Button feedback", + "preferences.hapticFeedback.description": "Sound/haptic feedback upon click", "preferences.transactionButtonOrder": "Button placement", "preferences.transactionButtonOrder.description": "Change new transaction button placement", "preferences.transactionButtonOrder.guide": "Drag the buttons to reorder", diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index 6fe071c..c6d6ed2 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -110,6 +110,7 @@ "transaction.description": "Note", "transaction.description.add": "Aggiungi note", "transaction.description.markdownSupported": "Markdown supportato", + "transaction.description.preview": "Anteprima", "transaction.location": "Posizione", "transaction.location.add": "Aggiungi una posizione", "transaction.location.edit": "Tocca la mappa per modificare", @@ -164,8 +165,8 @@ "preferences.numpad.layout": "Layout del tastierino numerico", "preferences.numpad.layout.classic": "Classico", "preferences.numpad.layout.modern": "Moderno", - "preferences.numpad.haptics": "Feedback dei tasti", - "preferences.numpad.haptics.description": "Feedback sonoro/tattile alla pressione", + "preferences.hapticFeedback": "Feedback dei tasti", + "preferences.hapticFeedback.description": "Feedback sonoro/tattile alla pressione", "preferences.transactionButtonOrder": "Posizione pulsanti", "preferences.transactionButtonOrder.description": "Modifica la posizione del pulsante per le nuove transazioni", "preferences.transactionButtonOrder.guide": "Trascina i pulsanti per riordinare", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index 0a2a123..348e318 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -110,6 +110,7 @@ "transaction.description": "Тэмдэглэл", "transaction.description.add": "Тэмдэглэл нэмэх", "transaction.description.markdownSupported": "Markdown ашиглах боломжтой", + "transaction.description.preview": "Харах", "transaction.location": "Байршил", "transaction.location.add": "Байршил нэмэх", "transaction.location.edit": "Газын зурган дээр товшиж байршлыг өөрчлөөрэй", @@ -158,14 +159,14 @@ "preferences.theme.light": "Гэгээлэг", "preferences.theme.dark": "Харанхуй", "preferences.theme.other": "Бусад үзэмжүүд", - "preferences.theme.themeChangesAppIcon": "Аппын дүрсийг үзэмж дагах", + "preferences.theme.themeChangesAppIcon": "Аппын дүрс дагаж өөрчлөх", "preferences.theme.enableDynamicTheme": "Динамик үзэмж", "preferences.numpad": "Тоон товчлуур", "preferences.numpad.layout": "Тооны байрлал", "preferences.numpad.layout.classic": "Хуучны", "preferences.numpad.layout.modern": "Орчин үеийн", - "preferences.numpad.haptics": "Товчны хариу", - "preferences.numpad.haptics.description": "Дарах үед дуу/чичиргээ тоглуулна", + "preferences.hapticFeedback": "Товчны хариу", + "preferences.hapticFeedback.description": "Дарах үед дуу/чичиргээ тоглуулна", "preferences.transactionButtonOrder": "Товчны байрлал", "preferences.transactionButtonOrder.description": "Шинэ гүйлгээ хийх товчны байрлал өөрчлөх", "preferences.transactionButtonOrder.guide": "Чирж байрлалыг өөрчлөөрэй", diff --git a/lib/prefs.dart b/lib/prefs.dart index 70ec5b8..e3e0ec1 100644 --- a/lib/prefs.dart +++ b/lib/prefs.dart @@ -32,8 +32,8 @@ class LocalPreferences { /// in a modern dialpad late final BoolSettingsEntry usePhoneNumpadLayout; - /// Whether to enable haptic feedback on numpad touch - late final BoolSettingsEntry enableNumpadHapticFeedback; + /// Whether to enable haptic feedback upon certain actions + late final BoolSettingsEntry enableHapticFeedback; /// Whether to combine transfer transactions in the transaction list /// @@ -96,8 +96,8 @@ class LocalPreferences { preferences: _prefs, initialValue: false, ); - enableNumpadHapticFeedback = BoolSettingsEntry( - key: "enableNumpadHapticFeedback", + enableHapticFeedback = BoolSettingsEntry( + key: "enableHapticFeedback", preferences: _prefs, initialValue: true, ); diff --git a/lib/routes.dart b/lib/routes.dart index 373e58f..6f9ac2a 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -13,6 +13,7 @@ import "package:flow/routes/home_page.dart"; import "package:flow/routes/import_page.dart"; import "package:flow/routes/import_wizard/v1.dart"; import "package:flow/routes/preferences/button_order_preferences_page.dart"; +import "package:flow/routes/preferences/haptics_preferences_page.dart"; import "package:flow/routes/preferences/money_formatting_preferences_page.dart"; import "package:flow/routes/preferences/numpad_preferences_page.dart"; import "package:flow/routes/preferences/pending_transactions.dart"; @@ -176,6 +177,10 @@ final router = GoRouter( path: "moneyFormatting", builder: (context, state) => const MoneyFormattingPreferencesPage(), ), + GoRoute( + path: "haptics", + builder: (context, state) => const HapticsPreferencesPage(), + ), ], ), GoRoute( diff --git a/lib/routes/new_transaction/description_section.dart b/lib/routes/new_transaction/description_section.dart index 288d6c1..792a8a2 100644 --- a/lib/routes/new_transaction/description_section.dart +++ b/lib/routes/new_transaction/description_section.dart @@ -2,12 +2,13 @@ import "dart:developer"; import "package:flow/entity/transaction.dart"; import "package:flow/l10n/extensions.dart"; -import "package:flow/routes/utils/edit_markdown_page.dart"; import "package:flow/routes/new_transaction/section.dart"; +import "package:flow/routes/utils/edit_markdown_page.dart"; import "package:flow/theme/theme.dart"; import "package:flow/utils/extensions/toast.dart"; import "package:flow/utils/open_url.dart"; import "package:flow/widgets/general/frame.dart"; +import "package:flow/widgets/general/markdown_view.dart"; import "package:flutter/material.dart"; import "package:flutter_markdown/flutter_markdown.dart"; import "package:go_router/go_router.dart"; @@ -31,8 +32,6 @@ class DescriptionSection extends StatelessWidget { Widget build(BuildContext context) { final bool noContent = controller.text.trim().isEmpty; - int checkboxCounter = 0; - return Section( titleOverride: Row( mainAxisSize: MainAxisSize.min, @@ -64,35 +63,11 @@ class DescriptionSection extends StatelessWidget { ) : Stack( children: [ - Frame( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - color: context.colorScheme.secondary, - ), - child: Markdown( - data: controller.text, - selectable: false, - shrinkWrap: true, - styleSheet: getStyleSheet(context), - checkboxBuilder: (value) { - final int index = checkboxCounter++; - - return Checkbox.adaptive( - value: value, - onChanged: (newValue) => { - tryFlipCheckbox(index, newValue ?? !value), - }, - ); - }, - onTapLink: (text, href, title) => onTapLink( - context, - text, - href, - title, - ), - ), - ), + MarkdownView( + controller: controller, + onChanged: onChanged, + focusNode: focusNode, + allowTogglingCheckboxes: true, ), Positioned( right: 24.0, diff --git a/lib/routes/new_transaction/input_amount_sheet.dart b/lib/routes/new_transaction/input_amount_sheet.dart index dfc6cfd..74bc689 100644 --- a/lib/routes/new_transaction/input_amount_sheet.dart +++ b/lib/routes/new_transaction/input_amount_sheet.dart @@ -334,7 +334,9 @@ class _InputAmountSheetState extends State value = value.removeDecimal(); } else { if (value.wholePart.abs() == 0) { - HapticFeedback.heavyImpact(); + if (LocalPreferences().enableHapticFeedback.get()) { + HapticFeedback.heavyImpact(); + } } value = value.removeWhole(); } diff --git a/lib/routes/new_transaction/title_input.dart b/lib/routes/new_transaction/title_input.dart index 40f2426..1456837 100644 --- a/lib/routes/new_transaction/title_input.dart +++ b/lib/routes/new_transaction/title_input.dart @@ -54,6 +54,7 @@ class TitleInput extends StatelessWidget { textAlign: TextAlign.center, maxLength: Transaction.maxTitleLength, onSubmitted: onSubmitted, + autocorrect: false, decoration: InputDecoration( hintText: fallbackTitle, counter: const SizedBox.shrink(), diff --git a/lib/routes/preferences/haptics_preferences_page.dart b/lib/routes/preferences/haptics_preferences_page.dart new file mode 100644 index 0000000..19c467d --- /dev/null +++ b/lib/routes/preferences/haptics_preferences_page.dart @@ -0,0 +1,48 @@ +import "package:flow/l10n/extensions.dart"; +import "package:flow/prefs.dart"; +import "package:flutter/material.dart"; + +class HapticsPreferencesPage extends StatefulWidget { + const HapticsPreferencesPage({super.key}); + + @override + State createState() => _HapticsPreferencesPageState(); +} + +class _HapticsPreferencesPageState extends State { + @override + Widget build(BuildContext context) { + final bool enableHapticFeedback = + LocalPreferences().enableHapticFeedback.get(); + + return Scaffold( + appBar: AppBar( + title: Text("preferences.hapticFeedback".t(context)), + ), + body: SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16.0), + CheckboxListTile.adaptive( + title: + Text("preferences.hapticFeedback.description".t(context)), + value: enableHapticFeedback, + onChanged: updateEnableHapticFeedback, + ), + ], + ), + ), + ), + ); + } + + void updateEnableHapticFeedback(bool? newEnableHapticFeedback) async { + if (newEnableHapticFeedback == null) return; + + await LocalPreferences().enableHapticFeedback.set(newEnableHapticFeedback); + + if (mounted) setState(() {}); + } +} diff --git a/lib/routes/preferences/numpad_preferences_page.dart b/lib/routes/preferences/numpad_preferences_page.dart index 0c56954..23e8b50 100644 --- a/lib/routes/preferences/numpad_preferences_page.dart +++ b/lib/routes/preferences/numpad_preferences_page.dart @@ -16,8 +16,6 @@ class _NumpadPreferencesPageState extends State { Widget build(BuildContext context) { final bool usePhoneNumpadLayout = LocalPreferences().usePhoneNumpadLayout.get(); - final bool enableNumpadHapticFeedback = - LocalPreferences().enableNumpadHapticFeedback.get(); return Scaffold( appBar: AppBar( @@ -52,14 +50,6 @@ class _NumpadPreferencesPageState extends State { ), ), const SizedBox(height: 32.0), - CheckboxListTile.adaptive( - title: Text("preferences.numpad.haptics".t(context)), - value: enableNumpadHapticFeedback, - onChanged: updateHapticUsage, - subtitle: - Text("preferences.numpad.haptics.description".t(context)), - ), - const SizedBox(height: 16.0), ], ), ), @@ -72,11 +62,4 @@ class _NumpadPreferencesPageState extends State { if (mounted) setState(() {}); } - - void updateHapticUsage(bool? enableHaptics) async { - if (enableHaptics == null) return; - - await LocalPreferences().enableNumpadHapticFeedback.set(enableHaptics); - if (mounted) setState(() {}); - } } diff --git a/lib/routes/preferences_page.dart b/lib/routes/preferences_page.dart index f683dcd..f20767c 100644 --- a/lib/routes/preferences_page.dart +++ b/lib/routes/preferences_page.dart @@ -29,6 +29,8 @@ class _PreferencesPageState extends State { getTheme(LocalPreferences().themeName.get()); final bool enableGeo = LocalPreferences().enableGeo.get(); + final bool enableHapticFeedback = + LocalPreferences().enableHapticFeedback.get(); final bool autoAttachTransactionGeo = LocalPreferences().autoAttachTransactionGeo.get(); final bool requirePendingTransactionConfrimation = @@ -136,6 +138,19 @@ class _PreferencesPageState extends State { onTap: () => pushAndRefreshAfter("/preferences/privacy"), trailing: const Icon(Symbols.chevron_right_rounded), ), + ListTile( + title: Text("preferences.hapticFeedback".t(context)), + leading: const Icon(Symbols.vibration_rounded), + onTap: () => pushAndRefreshAfter("/preferences/haptics"), + subtitle: Text( + enableHapticFeedback + ? "general.enabled".t(context) + : "general.disabled".t(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: const Icon(Symbols.chevron_right_rounded), + ), ]), ), ); diff --git a/lib/routes/utils/edit_markdown_page.dart b/lib/routes/utils/edit_markdown_page.dart index c406674..ffb00b0 100644 --- a/lib/routes/utils/edit_markdown_page.dart +++ b/lib/routes/utils/edit_markdown_page.dart @@ -1,6 +1,7 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/theme/theme.dart"; import "package:flow/widgets/general/form_close_button.dart"; +import "package:flow/widgets/general/markdown_view.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:go_router/go_router.dart"; @@ -26,13 +27,29 @@ class EditMarkdownPage extends StatefulWidget { State createState() => _EditMarkdownPageState(); } -class _EditMarkdownPageState extends State { +class _EditMarkdownPageState extends State + with SingleTickerProviderStateMixin { late final TextEditingController _controller; + late final TabController _tabController; + final FocusNode _focusNode = FocusNode(); + + bool focused = false; @override void initState() { super.initState(); _controller = TextEditingController(text: widget.initialValue); + _tabController = TabController(length: 2, vsync: this); + _focusNode.addListener(_handleFocusChange); + focused = _focusNode.hasFocus; + } + + @override + void dispose() { + _controller.dispose(); + _tabController.dispose(); + _focusNode.removeListener(_handleFocusChange); + super.dispose(); } @override @@ -55,25 +72,75 @@ class _EditMarkdownPageState extends State { tooltip: "general.save".t(context), ) ], + bottom: TabBar( + tabs: [ + Tab(text: "general.edit".t(context)), + Tab(text: "transaction.description.preview".t(context)), + ], + controller: _tabController, + ), centerTitle: true, backgroundColor: context.colorScheme.surface, ), - body: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: TextFormField( - decoration: InputDecoration( - hintText: "transaction.description".t(context), - border: OutlineInputBorder(), - counter: counterOverride, + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + TabBarView( + controller: _tabController, + children: [ + SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: TextFormField( + decoration: InputDecoration( + hintText: "transaction.description".t(context), + border: OutlineInputBorder(), + counter: counterOverride, + ), + focusNode: _focusNode, + keyboardType: TextInputType.multiline, + maxLines: null, + maxLength: widget.maxLength, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + minLines: 10, + controller: _controller, + autofocus: true, + textInputAction: TextInputAction.newline, + ), + ), + SingleChildScrollView( + padding: EdgeInsets.only(top: 16.0), + child: MarkdownView( + controller: _controller, + ), + ) + ], ), - keyboardType: TextInputType.multiline, - maxLines: null, - maxLength: widget.maxLength, - maxLengthEnforcement: MaxLengthEnforcement.enforced, - controller: _controller, - autofocus: true, - textInputAction: TextInputAction.newline, - ), + Positioned( + left: 0, + right: 0, + bottom: MediaQuery.of(context).viewInsets.bottom, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + child: Row( + children: [ + IconButton( + onPressed: _bold, + icon: Icon(Symbols.format_bold_rounded), + ), + IconButton( + onPressed: _italic, + icon: Icon(Symbols.format_italic_rounded), + ), + IconButton( + onPressed: _checklist, + icon: Icon(Symbols.checklist_rounded), + ), + ], + ), + ), + ), + ], ), ); } @@ -85,4 +152,104 @@ class _EditMarkdownPageState extends State { bool hasChanged() { return _controller.text != widget.initialValue; } + + _bold() { + if (_controller.selection.isCollapsed) { + _insert("****", -2); + } else { + _alterUncollapsed("**", "**"); + } + } + + _italic() { + if (_controller.selection.isCollapsed) { + _insert("**", -1); + } else { + _alterUncollapsed("*", "*"); + } + } + + _checklist() { + if (_controller.selection.isCollapsed) { + _insertChecklist(); + } else { + _alterUncollapsed("\n- [ ] ", "\n"); + } + } + + _alterUncollapsed(String prefix, String postfix, [int cursorOffset = 0]) { + try { + final TextSelection selection = _controller.selection; + final String text = _controller.text; + + if (!selection.isValid) return; + + // TODO bold the whole line if selection is collapsed + if (selection.isCollapsed) return; + + _controller.value = TextEditingValue( + text: + "${selection.textBefore(text)}$prefix${selection.textInside(text)}$postfix${selection.textAfter(text)}", + selection: TextSelection.collapsed( + offset: selection.end + prefix.length + postfix.length + cursorOffset, + affinity: selection.affinity, + ), + ); + } finally { + _focusNode.requestFocus(); + } + } + + _insertChecklist() { + try { + final TextSelection selection = _controller.selection; + final String text = _controller.text; + + if (!selection.isValid) return; + + final bool currentlyAtBegginingOfLine = + selection.start == 0 || text[selection.start - 1] == "\n"; + + final String payload = + currentlyAtBegginingOfLine ? "- [ ] \n" : "\n- [ ] \n"; + final int cursorOffset = -1; + + _controller.value = TextEditingValue( + text: + "${selection.textBefore(text)}$payload${selection.textAfter(text)}", + selection: TextSelection.collapsed( + offset: selection.end + payload.length + cursorOffset, + affinity: selection.affinity, + ), + ); + } finally { + _focusNode.requestFocus(); + } + } + + _insert(String payload, [int cursorOffset = 0]) { + try { + final TextSelection selection = _controller.selection; + final String text = _controller.text; + + if (!selection.isValid) return; + + _controller.value = TextEditingValue( + text: + "${selection.textBefore(text)}$payload${selection.textAfter(text)}", + selection: TextSelection.collapsed( + offset: selection.end + payload.length + cursorOffset, + affinity: selection.affinity, + ), + ); + } finally { + _focusNode.requestFocus(); + } + } + + _handleFocusChange() { + setState(() { + focused = _focusNode.hasFocus; + }); + } } diff --git a/lib/utils/extensions/toast.dart b/lib/utils/extensions/toast.dart index bda5e21..548ea9e 100644 --- a/lib/utils/extensions/toast.dart +++ b/lib/utils/extensions/toast.dart @@ -1,4 +1,5 @@ import "package:flow/l10n/localized_exception.dart"; +import "package:flow/prefs.dart"; import "package:flow/theme/theme.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; @@ -38,7 +39,9 @@ extension ToastHelper on BuildContext { } if (type == ToastificationType.error) { - HapticFeedback.heavyImpact(); + if (LocalPreferences().enableHapticFeedback.get()) { + HapticFeedback.heavyImpact(); + } } return toastification.show( diff --git a/lib/utils/numpad_haptic.dart b/lib/utils/numpad_haptic.dart deleted file mode 100644 index 867c77a..0000000 --- a/lib/utils/numpad_haptic.dart +++ /dev/null @@ -1,5 +0,0 @@ -import "package:flutter/services.dart"; - -void numpadHaptic() { - HapticFeedback.mediumImpact(); -} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 92efdc9..0181428 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -2,7 +2,6 @@ export "extensions.dart"; export "is_desktop.dart"; export "jasonable.dart"; export "number_formatting.dart"; -export "numpad_haptic.dart"; export "open_url.dart"; export "optional.dart"; export "pick_file.dart"; diff --git a/lib/widgets/general/markdown_view.dart b/lib/widgets/general/markdown_view.dart new file mode 100644 index 0000000..26e65a8 --- /dev/null +++ b/lib/widgets/general/markdown_view.dart @@ -0,0 +1,172 @@ +import "dart:developer"; + +import "package:flow/l10n/extensions.dart"; +import "package:flow/theme/theme.dart"; +import "package:flow/utils/extensions/toast.dart"; +import "package:flow/utils/open_url.dart"; +import "package:flow/widgets/general/frame.dart"; +import "package:flutter/material.dart"; +import "package:flutter_markdown/flutter_markdown.dart"; + +class MarkdownView extends StatelessWidget { + final TextEditingController controller; + final FocusNode? focusNode; + + final Function(String)? onChanged; + + final bool allowTogglingCheckboxes; + + const MarkdownView({ + super.key, + required this.controller, + this.focusNode, + this.onChanged, + this.allowTogglingCheckboxes = false, + }); + + @override + Widget build(BuildContext context) { + int checkboxCounter = 0; + + return Frame( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + color: context.colorScheme.secondary, + ), + child: Markdown( + data: controller.text, + selectable: false, + shrinkWrap: true, + styleSheet: getStyleSheet(context), + checkboxBuilder: (value) { + final int index = checkboxCounter++; + + return Checkbox.adaptive( + value: value, + onChanged: (newValue) => { + tryFlipCheckbox(index, newValue ?? !value), + }, + ); + }, + onTapLink: (text, href, title) => onTapLink( + context, + text, + href, + title, + ), + ), + ), + ); + } + + void tryFlipCheckbox(int index, bool value) { + if (!allowTogglingCheckboxes) { + log("[Flow] Cannot flip checkbox when toggling is disabled"); + return; + } + + if (controller.text.contains("```")) { + log("[Flow] Cannot flip checkbox when markdown contains a code block"); + return; + } + + log("[Flow] Flipping checkbox at [$index] to $value"); + + try { + final RegExpMatch match = RegExp(r"-\s\[(\s|x)\]", multiLine: true) + .allMatches(controller.text) + .elementAt(index); + + final String replacement = value ? "- [x]" : "- [ ]"; + + final String newText = controller.text.replaceRange( + match.start, + match.end, + replacement, + ); + + if (newText.length != controller.text.length) { + throw Exception("Length mismatch"); + } + + controller.text = newText; + + if (onChanged != null) { + onChanged!(newText); + } + } catch (e) { + log("[Flow] Failed to flip checkbox at [$index]", error: e); + } + } + + void onTapLink( + BuildContext context, + String text, + String? href, + String title, + ) { + log("[Flow] Tapped link: $text, $href, $title"); + + if (href == null) { + context.showErrorToast( + error: "error.url.cannotOpen".t(context), + ); + return; + } + + final Uri? parsed = Uri.tryParse(href); + if (parsed == null) { + context.showErrorToast( + error: "error.url.cannotOpen".t(context), + ); + return; + } + + openUrl(parsed).then((succeeded) { + if (!succeeded && context.mounted) { + context.showErrorToast( + error: "error.url.cannotOpen".t(context), + ); + } + }); + } + + MarkdownStyleSheet getStyleSheet(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + final TextStyle p = textTheme.bodyLarge!; + + return MarkdownStyleSheet( + h1: textTheme.headlineLarge! + .copyWith(fontSize: textTheme.headlineLarge!.fontSize! * 1.4), + h2: textTheme.headlineLarge! + .copyWith(fontSize: textTheme.headlineLarge!.fontSize! * 1.28), + h3: textTheme.headlineLarge! + .copyWith(fontSize: textTheme.headlineLarge!.fontSize! * 1.14), + h4: textTheme.headlineLarge, + h5: textTheme.headlineMedium, + h6: textTheme.headlineSmall, + p: p, + a: p.copyWith(color: context.colorScheme.primary), + strong: p.copyWith(fontWeight: FontWeight.bold), + em: p.copyWith(fontStyle: FontStyle.italic), + code: p.copyWith(fontFamily: "monospace"), + img: p.copyWith(fontStyle: FontStyle.italic), + checkbox: p.copyWith(fontFamily: "monospace"), + del: p.copyWith(decoration: TextDecoration.lineThrough), + blockquoteDecoration: BoxDecoration( + border: Border( + left: BorderSide( + color: context.colorScheme.onSurface.withAlpha(0x80), + width: 4.0, + ), + ), + ), + blockquotePadding: const EdgeInsets.only(left: 24.0), + codeblockDecoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(4.0), + ), + ); + } +} diff --git a/lib/widgets/general/money_text.dart b/lib/widgets/general/money_text.dart index 03bb0aa..7be0cf7 100644 --- a/lib/widgets/general/money_text.dart +++ b/lib/widgets/general/money_text.dart @@ -1,8 +1,10 @@ import "package:auto_size_text/auto_size_text.dart"; import "package:flow/data/money.dart"; +import "package:flow/prefs.dart"; import "package:flow/widgets/general/money_text_builder.dart"; import "package:flow/widgets/general/money_text_raw.dart"; import "package:flutter/material.dart"; +import "package:flutter/services.dart"; class MoneyText extends StatefulWidget { final Money? money; @@ -116,6 +118,10 @@ class _MoneyTextState extends State { if (widget.tapToToggleAbbreviation) { abbreviate = !abbreviate; + if (LocalPreferences().enableHapticFeedback.get()) { + HapticFeedback.lightImpact(); + } + if (mounted) setState(() => {}); } diff --git a/lib/widgets/numpad_button.dart b/lib/widgets/numpad_button.dart index 6c9b994..cc17651 100644 --- a/lib/widgets/numpad_button.dart +++ b/lib/widgets/numpad_button.dart @@ -1,7 +1,7 @@ import "package:flow/prefs.dart"; import "package:flow/theme/theme.dart"; -import "package:flow/utils/utils.dart"; import "package:flutter/material.dart"; +import "package:flutter/services.dart"; import "package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart"; class NumpadButton extends StatelessWidget { @@ -58,8 +58,8 @@ class NumpadButton extends StatelessWidget { } void onTapHandler() { - if (LocalPreferences().enableNumpadHapticFeedback.get()) { - numpadHaptic(); + if (LocalPreferences().enableHapticFeedback.get()) { + HapticFeedback.mediumImpact(); } if (onTap != null) { diff --git a/lib/widgets/transactions_date_header.dart b/lib/widgets/transactions_date_header.dart index 685d7aa..9da57dd 100644 --- a/lib/widgets/transactions_date_header.dart +++ b/lib/widgets/transactions_date_header.dart @@ -7,6 +7,7 @@ import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; import "package:flow/services/exchange_rates.dart"; import "package:flow/theme/theme.dart"; +import "package:flutter/services.dart"; import "package:flutter/widgets.dart"; import "package:moment_dart/moment_dart.dart"; @@ -48,6 +49,7 @@ class TransactionListDateHeader extends StatefulWidget { class _TransactionListDateHeaderState extends State { bool obscure = false; + bool rangeTitleAlternative = false; @override void initState() { @@ -66,8 +68,11 @@ class _TransactionListDateHeaderState extends State { @override Widget build(BuildContext context) { - final Widget title = - widget.titleOverride ?? Text(_getRangeTitle(widget.range)); + final Widget title = widget.titleOverride ?? + GestureDetector( + onTap: _handleRangeTextTap, + child: Text(_getRangeTitle()), + ); final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); @@ -134,10 +139,22 @@ class _TransactionListDateHeaderState extends State { setState(() {}); } - String _getRangeTitle(TimeRange range) { - return switch (range) { - DayTimeRange() => range.from.toMoment().calendar(omitHours: true), - _ => range.format(), + void _handleRangeTextTap() { + rangeTitleAlternative = !rangeTitleAlternative; + + if (LocalPreferences().enableHapticFeedback.get()) { + HapticFeedback.lightImpact(); + } + + setState(() {}); + } + + String _getRangeTitle() { + return switch ((widget.range, rangeTitleAlternative)) { + (DayTimeRange dayTimeRange, false) => + dayTimeRange.from.toMoment().calendar(omitHours: true), + (DayTimeRange dayTimeRange, true) => dayTimeRange.from.toMoment().ll, + (TimeRange other, _) => other.format(), }; } } diff --git a/lib/widgets/utils/utils.dart b/lib/widgets/utils/utils.dart index 9585ef9..7445e3b 100644 --- a/lib/widgets/utils/utils.dart +++ b/lib/widgets/utils/utils.dart @@ -7,7 +7,6 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/routes/utils/crop_square_image_page.dart"; import "package:flow/utils/extensions/toast.dart"; import "package:flutter/material.dart"; -import "package:flutter/services.dart"; import "package:go_router/go_router.dart"; import "package:image_picker/image_picker.dart"; import "package:url_launcher/url_launcher.dart"; @@ -27,10 +26,6 @@ Future openUrl( } } -void numpadHaptic() { - HapticFeedback.mediumImpact(); -} - Future pickFile() async { FilePickerResult? result = await FilePicker.platform.pickFiles(); diff --git a/pubspec.yaml b/pubspec.yaml index cfc7e79..71d1e63 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.9.0+83" +version: "0.9.0+84" environment: sdk: ">=3.5.0 <4.0.0" From 0fd3160b2e88b558f6b280fa04060bb464997746 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 1 Dec 2024 01:55:21 +0800 Subject: [PATCH 17/19] another iteration for 0.9.0 --- CHANGELOG.md | 3 + lib/objectbox/actions.dart | 28 ++++++ lib/routes/account_page.dart | 89 +++++++++++++++---- lib/routes/category_page.dart | 71 +++++++++++---- lib/routes/home/home_tab.dart | 2 +- .../transaction_context_actions.dart | 4 + .../pending_transactions_header.dart | 0 lib/widgets/grouped_transaction_list.dart | 1 + lib/widgets/home/home/flow_cards.dart | 9 +- lib/widgets/theme_petal_selector.dart | 4 - lib/widgets/transaction_list_tile.dart | 65 +++++++++----- pubspec.yaml | 2 +- 12 files changed, 218 insertions(+), 60 deletions(-) rename lib/widgets/{home/home => general}/pending_transactions_header.dart (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043057f..720d21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Home screen day title flow now converts all currencies into primary * Markdown editor (transaction description) now has a preview, and minimal toolbar +* Now you can duplicate a transaction be swiping to the right, closes [#232](https://github.com/flow-mn/flow/issues/232) ### Changes @@ -25,6 +26,8 @@ * Transaction list tile title color is now fixed in light themes * Wavy divider color now follows the theme change * Disabled Autocorrect on transaction title, so it no longer alters suggestions +* Category and account detail page now doesn't include pending transactions in + count and flow. ## Beta 0.8.1 diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index 8184819..56710eb 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -461,6 +461,34 @@ extension TransactionActions on Transaction { return false; } } + + void duplicate() { + if (isTransfer) { + throw Exception("Cannot duplicate transfer transactions"); + } + + final Transaction duplicate = Transaction( + amount: amount, + currency: currency, + title: title, + description: description, + transactionDate: transactionDate, + createdDate: Moment.now(), + isPending: isPending, + uuid: Uuid().v4(), + ) + ..setCategory(category.target) + ..setAccount(account.target); + + final List filteredExtensions = + extensions.data.where((ext) => ext is! Transfer).toList(); + + if (filteredExtensions.isNotEmpty) { + duplicate.addExtensions(filteredExtensions); + } + + ObjectBox().box().put(duplicate); + } } extension TransactionListActions on Iterable { diff --git a/lib/routes/account_page.dart b/lib/routes/account_page.dart index b9c7039..8e3ec55 100644 --- a/lib/routes/account_page.dart +++ b/lib/routes/account_page.dart @@ -1,5 +1,5 @@ import "package:auto_size_text/auto_size_text.dart"; -import "package:flow/data/money.dart"; +import "package:flow/data/exchange_rates.dart"; import "package:flow/data/money_flow.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/transaction.dart"; @@ -7,14 +7,20 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/objectbox/objectbox.g.dart"; +import "package:flow/prefs.dart"; import "package:flow/routes/error_page.dart"; +import "package:flow/services/exchange_rates.dart"; +import "package:flow/utils/utils.dart"; import "package:flow/widgets/category/transactions_info.dart"; import "package:flow/widgets/flow_card.dart"; import "package:flow/widgets/general/spinner.dart"; +import "package:flow/widgets/general/wavy_divider.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; -import "package:flow/widgets/transactions_date_header.dart"; +import "package:flow/widgets/general/pending_transactions_header.dart"; import "package:flow/widgets/no_result.dart"; +import "package:flow/widgets/rates_missing_warning.dart"; import "package:flow/widgets/time_range_selector.dart"; +import "package:flow/widgets/transactions_date_header.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:material_symbols_icons/symbols.dart"; @@ -80,6 +86,9 @@ class _AccountPageState extends State { if (this.account == null) return const ErrorPage(); final Account account = this.account!; + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + final ExchangeRates? rates = + ExchangeRatesService().getPrimaryCurrencyRates(); return StreamBuilder>( stream: qb(range) @@ -90,9 +99,33 @@ class _AccountPageState extends State { final bool noTransactions = (transactions?.length ?? 0) == 0; - final MoneyFlow flow = transactions?.flow ?? MoneyFlow(); - final Money totalIncome = flow.getIncomeByCurrency(account.currency); - final Money totalExpense = flow.getExpenseByCurrency(account.currency); + final DateTime now = Moment.now().startOfNextMinute(); + + final Map> grouped = transactions + ?.where((transaction) => + !transaction.transactionDate.isAfter(now) && + transaction.isPending != true) + .groupByDate() ?? + {}; + + final List pendingTransactions = transactions + ?.where((transaction) => + transaction.transactionDate.isAfter(now) || + transaction.isPending == true) + .toList() ?? + []; + + final int actionNeededCount = pendingTransactions + .where((transaction) => transaction.confirmable()) + .length; + + final Map> pendingTransactionsGrouped = + pendingTransactions.groupByRange( + rangeFn: (transaction) => + CustomTimeRange(Moment.minValue, Moment.maxValue), + ); + + final MoneyFlow flow = transactions?.nonPending.flow ?? MoneyFlow(); const double firstHeaderTopPadding = 0.0; @@ -105,8 +138,10 @@ class _AccountPageState extends State { ), const SizedBox(height: 8.0), TransactionsInfo( - count: transactions?.length, - flow: totalIncome + totalExpense, + count: transactions?.nonPending.length, + flow: rates == null + ? flow.getFlowByCurrency(primaryCurrency) + : flow.getTotalFlow(rates, primaryCurrency), icon: account.icon, ), const SizedBox(height: 12.0), @@ -114,7 +149,9 @@ class _AccountPageState extends State { children: [ Expanded( child: FlowCard( - flow: totalIncome, + flow: rates == null + ? flow.getIncomeByCurrency(primaryCurrency) + : flow.getTotalIncome(rates, primaryCurrency), type: TransactionType.income, autoSizeGroup: autoSizeGroup, ), @@ -122,13 +159,19 @@ class _AccountPageState extends State { const SizedBox(width: 12.0), Expanded( child: FlowCard( - flow: totalExpense, + flow: rates == null + ? flow.getExpenseByCurrency(primaryCurrency) + : flow.getTotalExpense(rates, primaryCurrency), type: TransactionType.expense, autoSizeGroup: autoSizeGroup, ), ), ], ), + if (rates == null) ...[ + const SizedBox(height: 12.0), + RatesMissingWarning(), + ], ], ); @@ -169,16 +212,30 @@ class _AccountPageState extends State { ), _ => GroupedTransactionList( header: header, - transactions: transactions?.groupByDate() ?? {}, + transactions: grouped, + pendingTransactions: pendingTransactionsGrouped, + pendingDivider: WavyDivider(), listPadding: widget.listPadding, headerPadding: widget.headerPadding, firstHeaderTopPadding: firstHeaderTopPadding, - headerBuilder: (pendingGroup, range, rangeTransactions) => - TransactionListDateHeader( - transactions: rangeTransactions, - range: range, - pendingGroup: pendingGroup, - ), + headerBuilder: ( + pendingGroup, + range, + transactions, + ) { + if (pendingGroup) { + return PendingTransactionsHeader( + transactions: transactions, + range: range, + badgeCount: actionNeededCount, + ); + } + + return TransactionListDateHeader( + transactions: transactions, + range: range, + ); + }, ) }, ), diff --git a/lib/routes/category_page.dart b/lib/routes/category_page.dart index 1be9685..0fa30d8 100644 --- a/lib/routes/category_page.dart +++ b/lib/routes/category_page.dart @@ -10,14 +10,17 @@ import "package:flow/objectbox/objectbox.g.dart"; import "package:flow/prefs.dart"; import "package:flow/routes/error_page.dart"; import "package:flow/services/exchange_rates.dart"; +import "package:flow/utils/utils.dart"; import "package:flow/widgets/category/transactions_info.dart"; import "package:flow/widgets/flow_card.dart"; import "package:flow/widgets/general/spinner.dart"; +import "package:flow/widgets/general/wavy_divider.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; -import "package:flow/widgets/transactions_date_header.dart"; +import "package:flow/widgets/general/pending_transactions_header.dart"; import "package:flow/widgets/no_result.dart"; import "package:flow/widgets/rates_missing_warning.dart"; import "package:flow/widgets/time_range_selector.dart"; +import "package:flow/widgets/transactions_date_header.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:material_symbols_icons/symbols.dart"; @@ -96,11 +99,35 @@ class _CategoryPageState extends State { final bool noTransactions = (transactions?.length ?? 0) == 0; - final MoneyFlow flow = transactions?.flow ?? MoneyFlow(); + final DateTime now = Moment.now().startOfNextMinute(); - const double firstHeaderTopPadding = 0.0; + final Map> grouped = transactions + ?.where((transaction) => + !transaction.transactionDate.isAfter(now) && + transaction.isPending != true) + .groupByDate() ?? + {}; - final bool missingRates = rates == null; + final List pendingTransactions = transactions + ?.where((transaction) => + transaction.transactionDate.isAfter(now) || + transaction.isPending == true) + .toList() ?? + []; + + final int actionNeededCount = pendingTransactions + .where((transaction) => transaction.confirmable()) + .length; + + final Map> pendingTransactionsGrouped = + pendingTransactions.groupByRange( + rangeFn: (transaction) => + CustomTimeRange(Moment.minValue, Moment.maxValue), + ); + + final MoneyFlow flow = transactions?.nonPending.flow ?? MoneyFlow(); + + const double firstHeaderTopPadding = 0.0; final Widget header = Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -111,7 +138,7 @@ class _CategoryPageState extends State { ), const SizedBox(height: 8.0), TransactionsInfo( - count: transactions?.length, + count: transactions?.nonPending.length, flow: rates == null ? flow.getFlowByCurrency(primaryCurrency) : flow.getTotalFlow(rates, primaryCurrency), @@ -122,7 +149,7 @@ class _CategoryPageState extends State { children: [ Expanded( child: FlowCard( - flow: missingRates + flow: rates == null ? flow.getIncomeByCurrency(primaryCurrency) : flow.getTotalIncome(rates, primaryCurrency), type: TransactionType.income, @@ -132,7 +159,7 @@ class _CategoryPageState extends State { const SizedBox(width: 12.0), Expanded( child: FlowCard( - flow: missingRates + flow: rates == null ? flow.getExpenseByCurrency(primaryCurrency) : flow.getTotalExpense(rates, primaryCurrency), type: TransactionType.expense, @@ -141,7 +168,7 @@ class _CategoryPageState extends State { ), ], ), - if (missingRates) ...[ + if (rates == null) ...[ const SizedBox(height: 12.0), RatesMissingWarning(), ], @@ -185,16 +212,30 @@ class _CategoryPageState extends State { ), _ => GroupedTransactionList( header: header, - transactions: transactions?.groupByDate() ?? {}, + transactions: grouped, + pendingTransactions: pendingTransactionsGrouped, + pendingDivider: WavyDivider(), listPadding: widget.listPadding, headerPadding: widget.headerPadding, firstHeaderTopPadding: firstHeaderTopPadding, - headerBuilder: (pendingGroup, range, rangeTransactions) => - TransactionListDateHeader( - transactions: rangeTransactions, - range: range, - pendingGroup: pendingGroup, - ), + headerBuilder: ( + pendingGroup, + range, + transactions, + ) { + if (pendingGroup) { + return PendingTransactionsHeader( + transactions: transactions, + range: range, + badgeCount: actionNeededCount, + ); + } + + return TransactionListDateHeader( + transactions: transactions, + range: range, + ); + }, ) }, ), diff --git a/lib/routes/home/home_tab.dart b/lib/routes/home/home_tab.dart index c2750ea..d234b24 100644 --- a/lib/routes/home/home_tab.dart +++ b/lib/routes/home/home_tab.dart @@ -11,7 +11,7 @@ import "package:flow/widgets/grouped_transaction_list.dart"; import "package:flow/widgets/home/greetings_bar.dart"; import "package:flow/widgets/home/home/flow_cards.dart"; import "package:flow/widgets/home/home/no_transactions.dart"; -import "package:flow/widgets/home/home/pending_transactions_header.dart"; +import "package:flow/widgets/general/pending_transactions_header.dart"; import "package:flow/widgets/rates_missing_warning.dart"; import "package:flow/widgets/transactions_date_header.dart"; import "package:flow/widgets/utils/time_and_range.dart"; diff --git a/lib/utils/extensions/transaction_context_actions.dart b/lib/utils/extensions/transaction_context_actions.dart index 5373c24..433c065 100644 --- a/lib/utils/extensions/transaction_context_actions.dart +++ b/lib/utils/extensions/transaction_context_actions.dart @@ -29,4 +29,8 @@ extension TransactionContextActions on BuildContext { transaction.confirm(confirm, updateTransactionDate); } + + Future duplicateTransaction(Transaction transaction) async { + transaction.duplicate(); + } } diff --git a/lib/widgets/home/home/pending_transactions_header.dart b/lib/widgets/general/pending_transactions_header.dart similarity index 100% rename from lib/widgets/home/home/pending_transactions_header.dart rename to lib/widgets/general/pending_transactions_header.dart diff --git a/lib/widgets/grouped_transaction_list.dart b/lib/widgets/grouped_transaction_list.dart index 7877120..20b4d0a 100644 --- a/lib/widgets/grouped_transaction_list.dart +++ b/lib/widgets/grouped_transaction_list.dart @@ -147,6 +147,7 @@ class _GroupedTransactionListState extends State { deleteFn: () => context.deleteTransaction(transaction), confirmFn: ([bool confirm = true]) => context.confirmTransaction(transaction, confirm), + duplicateFn: () => context.duplicateTransaction(transaction), overrideObscure: widget.overrideObscure, ), (_) => Container(), diff --git a/lib/widgets/home/home/flow_cards.dart b/lib/widgets/home/home/flow_cards.dart index 469a0e3..10466e7 100644 --- a/lib/widgets/home/home/flow_cards.dart +++ b/lib/widgets/home/home/flow_cards.dart @@ -10,6 +10,7 @@ import "package:flow/theme/theme.dart"; import "package:flow/widgets/general/money_text.dart"; import "package:flow/widgets/home/home/info_card.dart"; import "package:flutter/cupertino.dart"; +import "package:flutter/services.dart"; class FlowCards extends StatefulWidget { final List? transactions; @@ -115,7 +116,13 @@ class _FlowCardsState extends State { ); } - void handleTap() => setState(() => abbreviate = !abbreviate); + void handleTap() { + if (LocalPreferences().enableHapticFeedback.get()) { + HapticFeedback.lightImpact(); + } + + setState(() => abbreviate = !abbreviate); + } _updateAbbreviation() { abbreviate = !LocalPreferences().preferFullAmounts.get(); diff --git a/lib/widgets/theme_petal_selector.dart b/lib/widgets/theme_petal_selector.dart index 273aace..7f12c8f 100644 --- a/lib/widgets/theme_petal_selector.dart +++ b/lib/widgets/theme_petal_selector.dart @@ -125,10 +125,6 @@ class _ThemePetalSelectorState extends State ? SystemMouseCursors.click : MouseCursor.defer; - if (itemIndexAtPointer != null && widget.updateOnHover) { - setThemeByIndex(itemIndexAtPointer, isDark); - } - setState(() { hoveringIndex = itemIndexAtPointer; }); diff --git a/lib/widgets/transaction_list_tile.dart b/lib/widgets/transaction_list_tile.dart index 5c848a1..9c5e627 100644 --- a/lib/widgets/transaction_list_tile.dart +++ b/lib/widgets/transaction_list_tile.dart @@ -18,6 +18,7 @@ class TransactionListTile extends StatelessWidget { final EdgeInsets padding; final VoidCallback deleteFn; + final VoidCallback? duplicateFn; final Function([bool confirm])? confirmFn; final Key? dismissibleKey; @@ -33,6 +34,7 @@ class TransactionListTile extends StatelessWidget { required this.combineTransfers, this.padding = EdgeInsets.zero, this.confirmFn, + this.duplicateFn, this.dismissibleKey, this.overrideObscure, }); @@ -159,31 +161,50 @@ class TransactionListTile extends StatelessWidget { ), ); + final List startActionsPanes = [ + if (!transaction.isTransfer && duplicateFn != null) + SlidableAction( + onPressed: (context) => duplicateFn!(), + icon: Symbols.content_copy_rounded, + backgroundColor: context.flowColors.semi, + ) + ]; + + final List endActionPanes = [ + if (showPendingConfirmation) + SlidableAction( + onPressed: (context) => confirmFn!(), + icon: Symbols.check_rounded, + backgroundColor: context.colorScheme.primary, + ), + if (showHoldButton) + SlidableAction( + onPressed: (context) => confirmFn!(false), + icon: Symbols.cancel_rounded, + backgroundColor: context.flowColors.expense, + ), + if (!showHoldButton) + SlidableAction( + onPressed: (context) => deleteFn(), + icon: Symbols.delete_forever_rounded, + backgroundColor: context.flowColors.expense, + ) + ]; + return Slidable( key: dismissibleKey, - endActionPane: ActionPane( - motion: const DrawerMotion(), - children: [ - if (showPendingConfirmation) - SlidableAction( - onPressed: (context) => confirmFn!(), - icon: Symbols.check_rounded, - backgroundColor: context.colorScheme.primary, - ), - if (showHoldButton) - SlidableAction( - onPressed: (context) => confirmFn!(false), - icon: Symbols.cancel_rounded, - backgroundColor: context.flowColors.expense, - ), - if (!showHoldButton) - SlidableAction( - onPressed: (context) => deleteFn(), - icon: Symbols.delete_forever_rounded, - backgroundColor: context.flowColors.expense, + endActionPane: endActionPanes.isNotEmpty + ? ActionPane( + motion: const DrawerMotion(), + children: endActionPanes, ) - ], - ), + : null, + startActionPane: startActionsPanes.isNotEmpty + ? ActionPane( + motion: const DrawerMotion(), + children: startActionsPanes, + ) + : null, child: listTile, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 71d1e63..4187867 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.9.0+84" +version: "0.9.0+85" environment: sdk: ">=3.5.0 <4.0.0" From 57d499c88919e85d8c37c525ff589ea0b81d092f Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 1 Dec 2024 15:46:19 +0800 Subject: [PATCH 18/19] chore: Enhance search, fix karma :( --- CHANGELOG.md | 1 + lib/data/transactions_filter/search_data.dart | 2 +- lib/objectbox/actions.dart | 10 +++++++--- lib/routes/transaction_page.dart | 2 +- pubspec.yaml | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 720d21c..705ceb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * Disabled Autocorrect on transaction title, so it no longer alters suggestions * Category and account detail page now doesn't include pending transactions in count and flow. +* Home tab search should work better now ## Beta 0.8.1 diff --git a/lib/data/transactions_filter/search_data.dart b/lib/data/transactions_filter/search_data.dart index 36753a7..e98a688 100644 --- a/lib/data/transactions_filter/search_data.dart +++ b/lib/data/transactions_filter/search_data.dart @@ -54,7 +54,7 @@ class TransactionSearchData { final double score = t.titleSuggestionScore( query: normalizedKeyword, - fuzzyPartial: false, + fuzzyPartial: true, ); return score >= smartMatchThreshold; diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index 56710eb..522ff12 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -323,13 +323,17 @@ extension TransactionActions on Transaction { int? categoryId, TransactionType? transactionType, bool fuzzyPartial = true, + bool caseSensitive = false, }) { double score = 10.0; - if (query?.trim().isNotEmpty == true && title?.trim().isNotEmpty == true) { + final String? normalizedTitle = + caseSensitive ? title?.trim() : title?.trim().toLowerCase(); + + if (query?.trim().isNotEmpty == true && normalizedTitle != null) { score += fuzzyPartial - ? partialRatio(query!, title!).toDouble() - : ratio(query!, title!).toDouble(); + ? partialRatio(query!, normalizedTitle).toDouble() + : ratio(query!, normalizedTitle).toDouble(); } double multipler = 1.0; diff --git a/lib/routes/transaction_page.dart b/lib/routes/transaction_page.dart index a277ed3..354878e 100644 --- a/lib/routes/transaction_page.dart +++ b/lib/routes/transaction_page.dart @@ -739,7 +739,7 @@ class _TransactionPageState extends State { ]; final bool isPending = requirePendingTransactionConfrimation - ? _transactionDate.isFuture + ? _transactionDate.isFutureAnchored(Moment.now().startOfNextMinute()) : false; if (isTransfer) { diff --git a/pubspec.yaml b/pubspec.yaml index 4187867..ee978a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.9.0+85" +version: "0.9.0+86" environment: sdk: ">=3.5.0 <4.0.0" From f74f4f050ce77936c8a14e44cbfe7ec09de4c406 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Fri, 6 Dec 2024 00:11:18 +0800 Subject: [PATCH 19/19] Enahcnements --- lib/utils/extensions/transaction.dart | 24 ++++++++++++++---------- lib/widgets/transaction_list_tile.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/utils/extensions/transaction.dart b/lib/utils/extensions/transaction.dart index 6c05664..c494ea4 100644 --- a/lib/utils/extensions/transaction.dart +++ b/lib/utils/extensions/transaction.dart @@ -2,15 +2,19 @@ import "package:flow/entity/transaction.dart"; import "package:moment_dart/moment_dart.dart"; extension TransactionHelpers on Transaction { - bool confirmable([DateTime? anchor]) => - isPending == true && - transactionDate.isPastAnchored( - anchor ?? Moment.now().endOfNextMinute(), - ); + bool confirmable([DateTime? anchor]) { + if (isPending != true) return false; - bool holdable([DateTime? anchor]) => - isPending != true && - transactionDate.isFutureAnchored( - anchor ?? Moment.now().startOfMinute(), - ); + return transactionDate.isPastAnchored( + anchor ?? Moment.now().endOfNextMinute(), + ); + } + + bool holdable([DateTime? anchor]) { + if (isPending != true) return false; + + return transactionDate.isFutureAnchored( + anchor ?? Moment.now().startOfMinute(), + ); + } } diff --git a/lib/widgets/transaction_list_tile.dart b/lib/widgets/transaction_list_tile.dart index 9c5e627..1d1d600 100644 --- a/lib/widgets/transaction_list_tile.dart +++ b/lib/widgets/transaction_list_tile.dart @@ -171,7 +171,7 @@ class TransactionListTile extends StatelessWidget { ]; final List endActionPanes = [ - if (showPendingConfirmation) + if (confirmFn != null && transaction.isPending == true) SlidableAction( onPressed: (context) => confirmFn!(), icon: Symbols.check_rounded, diff --git a/pubspec.yaml b/pubspec.yaml index ee978a1..e801a36 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.9.0+86" +version: "0.9.0+87" environment: sdk: ">=3.5.0 <4.0.0"