From 7bf981f2c90024e2cc6679f2d8335a2c1e5e3814 Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sat, 23 Nov 2024 15:47:26 +0800 Subject: [PATCH] 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 44552873..dc7e9761 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 b1b7c8d5..986d9973 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 eb57f6bc..cf0ba1b4 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 abf06624..1faaa2fb 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 2fe537c1..bae937c8 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 335871b7..7b082089 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 7e302c72..94a0b0d4 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 abb97a62..654b5ad8 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 df9dfa0e..12b89724 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 33a6f156..4627dd0b 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 00000000..d7b18d67 --- /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 00000000..725a0db0 --- /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 b98f0675..8ebd120d 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 00000000..4e45abd6 --- /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 e7b1bc06..516d1c3a 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 235bb368..05ee9334 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, ), ],