diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml
new file mode 100644
index 00000000..b5e8cfd4
--- /dev/null
+++ b/.github/workflows/claude-code-review.yml
@@ -0,0 +1,44 @@
+name: Claude Code Review
+
+on:
+ pull_request:
+ types: [opened, synchronize, ready_for_review, reopened]
+ # Optional: Only run on specific file changes
+ # paths:
+ # - "src/**/*.ts"
+ # - "src/**/*.tsx"
+ # - "src/**/*.js"
+ # - "src/**/*.jsx"
+
+jobs:
+ claude-review:
+ # Optional: Filter by PR author
+ # if: |
+ # github.event.pull_request.user.login == 'external-contributor' ||
+ # github.event.pull_request.user.login == 'new-developer' ||
+ # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ issues: read
+ id-token: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code Review
+ id: claude-review
+ uses: anthropics/claude-code-action@v1
+ with:
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
+ plugins: 'code-review@claude-code-plugins'
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
+ # or https://code.claude.com/docs/en/cli-reference for available options
+
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
new file mode 100644
index 00000000..6b15fac7
--- /dev/null
+++ b/.github/workflows/claude.yml
@@ -0,0 +1,50 @@
+name: Claude Code
+
+on:
+ issue_comment:
+ types: [created]
+ pull_request_review_comment:
+ types: [created]
+ issues:
+ types: [opened, assigned]
+ pull_request_review:
+ types: [submitted]
+
+jobs:
+ claude:
+ if: |
+ (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
+ (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ issues: read
+ id-token: write
+ actions: read # Required for Claude to read CI results on PRs
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code
+ id: claude
+ uses: anthropics/claude-code-action@v1
+ with:
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+
+ # This is an optional setting that allows Claude to read CI results on PRs
+ additional_permissions: |
+ actions: read
+
+ # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
+ # prompt: 'Update the pull request description to include a summary of changes.'
+
+ # Optional: Add claude_args to customize behavior and configuration
+ # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
+ # or https://code.claude.com/docs/en/cli-reference for available options
+ # claude_args: '--allowed-tools Bash(gh pr *)'
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0647f30f..6278555b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## 0.21.0
+
+Special thanks to [@PawiX25](https://github.com/PawiX25) for the new widget!
+
+### New features
+
+* Added summary home screen widget showing monthly income and expenses by [@PawiX25](https://github.com/PawiX25)
+
+### Fixes
+
+* [Android] Two Entry Last now shows the correct buttons, fixed by [@PawiX25](https://github.com/PawiX25)
+
## 0.20.0
### New features
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a96522cd..20c7dc05 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -106,6 +106,16 @@
android:name="android.appwidget.provider"
android:resource="@xml/four_entry_widget_info" />
+
+
+
+
+
+
+ get() = HomeWidgetGlanceStateDefinition()
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ provideContent {
+ GlanceTheme {
+ Content(context, currentState())
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalGlancePreviewApi::class)
+@Composable
+@Preview(widthDp = 200, heightDp = 100)
+private fun Content(context: Context, currentState: HomeWidgetGlanceState) {
+ val income = currentState.preferences.getString("summaryIncome", null) ?: "---"
+ val expense = currentState.preferences.getString("summaryExpense", null) ?: "---"
+ val incomeLabel = currentState.preferences.getString("summaryIncomeLabel", null) ?: "Income"
+ val expenseLabel = currentState.preferences.getString("summaryExpenseLabel", null) ?: "Expense"
+
+ Box(
+ modifier = GlanceModifier
+ .background(GlanceTheme.colors.widgetBackground)
+ .fillMaxSize()
+ .clickable(
+ onClick = actionStartActivity(
+ Intent(context, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ )
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Row(
+ modifier = GlanceModifier.padding(8.dp).fillMaxSize(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(modifier = GlanceModifier.defaultWeight()) {
+ SummaryCard(
+ label = incomeLabel,
+ iconRes = R.drawable.ic_arrow_down_forward,
+ amount = income,
+ accentColor = ColorProvider(R.color.income_green),
+ )
+ }
+ Spacer(modifier = GlanceModifier.width(8.dp))
+ Box(modifier = GlanceModifier.defaultWeight()) {
+ SummaryCard(
+ label = expenseLabel,
+ iconRes = R.drawable.ic_arrow_up_forward,
+ amount = expense,
+ accentColor = ColorProvider(R.color.expense_red),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun SummaryCard(label: String, iconRes: Int, amount: String, accentColor: ColorProvider) {
+ Box(
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .background(GlanceTheme.colors.surfaceVariant)
+ .cornerRadius(16.dp)
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ ) {
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = label,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurfaceVariant,
+ fontSize = 13.sp,
+ ),
+ )
+ Spacer(modifier = GlanceModifier.width(4.dp))
+ Image(
+ provider = ImageProvider(iconRes),
+ contentDescription = null,
+ modifier = GlanceModifier.width(20.dp).height(20.dp),
+ colorFilter = ColorFilter.tint(accentColor),
+ )
+ }
+ Spacer(modifier = GlanceModifier.height(4.dp))
+ Text(
+ text = amount,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSurface,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold,
+ ),
+ maxLines = 1,
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/mn/flow/flow/glance/SummaryReceiver.kt b/android/app/src/main/kotlin/mn/flow/flow/glance/SummaryReceiver.kt
new file mode 100644
index 00000000..0d5f3a73
--- /dev/null
+++ b/android/app/src/main/kotlin/mn/flow/flow/glance/SummaryReceiver.kt
@@ -0,0 +1,8 @@
+package mn.flow.flow.glance
+
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+
+class SummaryReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = Summary()
+}
diff --git a/android/app/src/main/kotlin/mn/flow/flow/glance/TwoEntryLast.kt b/android/app/src/main/kotlin/mn/flow/flow/glance/TwoEntryLast.kt
index 9a7237cb..9e90cd4e 100644
--- a/android/app/src/main/kotlin/mn/flow/flow/glance/TwoEntryLast.kt
+++ b/android/app/src/main/kotlin/mn/flow/flow/glance/TwoEntryLast.kt
@@ -44,7 +44,7 @@ class TwoEntryLast : GlanceAppWidget() {
@Composable
@Preview(widthDp = 100, heightDp = 50)
private fun Content(context: Context, currentState: HomeWidgetGlanceState) {
- val buttonOrder = FlowWidgetUtils.getButtonOrder(currentState.preferences).subList(0, 2)
+ val buttonOrder = FlowWidgetUtils.getButtonOrder(currentState.preferences).takeLast(2)
val size = LocalSize.current
val buttonSize = min((size.width / 2 - 24.dp), (size.height - 16.dp))
diff --git a/android/app/src/main/res/drawable/ic_arrow_down_forward.xml b/android/app/src/main/res/drawable/ic_arrow_down_forward.xml
new file mode 100644
index 00000000..c5c47d2b
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_arrow_down_forward.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_arrow_up_forward.xml b/android/app/src/main/res/drawable/ic_arrow_up_forward.xml
new file mode 100644
index 00000000..602b927c
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_arrow_up_forward.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/values-night/widget_colors.xml b/android/app/src/main/res/values-night/widget_colors.xml
new file mode 100644
index 00000000..afda8985
--- /dev/null
+++ b/android/app/src/main/res/values-night/widget_colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF32CC70
+ #FFFF4040
+
diff --git a/android/app/src/main/res/values/widget_colors.xml b/android/app/src/main/res/values/widget_colors.xml
new file mode 100644
index 00000000..afda8985
--- /dev/null
+++ b/android/app/src/main/res/values/widget_colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF32CC70
+ #FFFF4040
+
diff --git a/android/app/src/main/res/xml/summary_widget_info.xml b/android/app/src/main/res/xml/summary_widget_info.xml
new file mode 100644
index 00000000..41b20736
--- /dev/null
+++ b/android/app/src/main/res/xml/summary_widget_info.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/ios/Flow Widgets/LeBundle.swift b/ios/Flow Widgets/LeBundle.swift
index 51abb0ab..c1547932 100644
--- a/ios/Flow Widgets/LeBundle.swift
+++ b/ios/Flow Widgets/LeBundle.swift
@@ -12,5 +12,6 @@ import SwiftUI
struct LeBundle: WidgetBundle {
var body: some Widget {
FlowTwoEntryWidget()
+ FlowSummaryWidget()
}
}
diff --git a/ios/Flow Widgets/SummaryWidget.swift b/ios/Flow Widgets/SummaryWidget.swift
new file mode 100644
index 00000000..b9281e20
--- /dev/null
+++ b/ios/Flow Widgets/SummaryWidget.swift
@@ -0,0 +1,109 @@
+import SwiftUI
+import WidgetKit
+
+struct SummaryWidgetEntry: TimelineEntry {
+ let date: Date
+ let income: String
+ let expense: String
+ let incomeLabel: String
+ let expenseLabel: String
+}
+
+struct SummaryProvider: TimelineProvider {
+ typealias Entry = SummaryWidgetEntry
+
+ func placeholder(in context: Context) -> SummaryWidgetEntry {
+ SummaryWidgetEntry(date: Date(), income: "---", expense: "---", incomeLabel: "Income", expenseLabel: "Expense")
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (SummaryWidgetEntry) -> ()) {
+ let prefs = UserDefaults(suiteName: "group.mn.flow.flow")
+ let income = prefs?.string(forKey: "summaryIncome") ?? "---"
+ let expense = prefs?.string(forKey: "summaryExpense") ?? "---"
+ let incomeLabel = prefs?.string(forKey: "summaryIncomeLabel") ?? "Income"
+ let expenseLabel = prefs?.string(forKey: "summaryExpenseLabel") ?? "Expense"
+ let entry = SummaryWidgetEntry(date: Date(), income: income, expense: expense, incomeLabel: incomeLabel, expenseLabel: expenseLabel)
+ completion(entry)
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
+ getSnapshot(in: context) { (entry) in
+ let timeline = Timeline(entries: [entry], policy: .atEnd)
+ completion(timeline)
+ }
+ }
+}
+
+struct SummaryWidgetView: View {
+ var entry: SummaryWidgetEntry
+
+ var body: some View {
+ HStack(spacing: 8) {
+ summaryCard(
+ label: entry.incomeLabel,
+ amount: entry.income,
+ icon: "Income",
+ color: .green
+ )
+ summaryCard(
+ label: entry.expenseLabel,
+ amount: entry.expense,
+ icon: "Expense",
+ color: .red
+ )
+ }
+ .padding(12)
+ }
+
+ @ViewBuilder
+ func summaryCard(label: String, amount: String, icon: String, color: Color) -> some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack(spacing: 4) {
+ Text(label)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Image(icon)
+ .resizable()
+ .renderingMode(.template)
+ .foregroundStyle(color)
+ .frame(width: 14, height: 14)
+ }
+ Text(amount)
+ .font(.system(.title3, design: .rounded, weight: .semibold))
+ .foregroundStyle(.primary)
+ .minimumScaleFactor(0.5)
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(10)
+ .background(.fill.tertiary, in: RoundedRectangle(cornerRadius: 12))
+ }
+}
+
+struct FlowSummaryWidget: Widget {
+ let kind: String = "FlowSummaryWidget"
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(
+ kind: kind, provider: SummaryProvider()
+ ) { entry in
+ SummaryWidgetView(entry: entry)
+ .containerBackground(.fill.tertiary, for: .widget)
+ }
+ .supportedFamilies([.systemSmall, .systemMedium])
+ .configurationDisplayName("Flow Summary")
+ .description("View your monthly income and expenses at a glance.")
+ }
+}
+
+#Preview(as: .systemSmall) {
+ FlowSummaryWidget()
+} timeline: {
+ SummaryWidgetEntry(date: .now, income: "$3.82K", expense: "$1.24K", incomeLabel: "Income", expenseLabel: "Expense")
+}
+
+#Preview(as: .systemMedium) {
+ FlowSummaryWidget()
+} timeline: {
+ SummaryWidgetEntry(date: .now, income: "$3.82K", expense: "$1.24K", incomeLabel: "Income", expenseLabel: "Expense")
+}
diff --git a/lib/l10n/flow_localizations.dart b/lib/l10n/flow_localizations.dart
index 740ea9db..d6047d34 100644
--- a/lib/l10n/flow_localizations.dart
+++ b/lib/l10n/flow_localizations.dart
@@ -1,6 +1,8 @@
+import "dart:async";
import "dart:convert";
import "package:flow/l10n/supported_languages.dart";
+import "package:flow/services/widget_summary_sync.dart";
import "package:flutter/services.dart";
import "package:flutter/widgets.dart";
import "package:logging/logging.dart";
@@ -136,6 +138,7 @@ class _FlowLocalizationDelegate
: FlowLocalizations.supportedLocales[1],
);
await localization.load();
+ unawaited(WidgetSummarySync.sync().catchError((_) {}));
return localization;
}
diff --git a/lib/main.dart b/lib/main.dart
index 1bd654e8..ec88eb70 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -42,6 +42,7 @@ import "package:flow/services/recurring_transactions.dart";
import "package:flow/services/sync.dart";
import "package:flow/services/transactions.dart";
import "package:flow/services/user_preferences.dart";
+import "package:flow/services/widget_summary_sync.dart";
import "package:flow/theme/color_themes/registry.dart";
import "package:flow/theme/flow_color_scheme.dart";
import "package:flow/theme/theme.dart";
@@ -149,6 +150,8 @@ void main() async {
);
}
+ TransactionsService().addListener(() => WidgetSummarySync.sync());
+
try {
Moment.minValue = DateTime(0);
Moment.maxValue = DateTime(4000);
@@ -199,6 +202,9 @@ class FlowState extends State {
UserPreferencesService().valueNotifier.addListener(_reloadTheme);
UserPreferencesService().valueNotifier.addListener(_listenToShakes);
+ UserPreferencesService().valueNotifier.addListener(_syncWidgets);
+
+ ExchangeRatesService().exchangeRatesCache.addListener(_syncWidgets);
LocalPreferences().localeOverride.addListener(_reloadLocale);
LocalPreferences().primaryCurrency.addListener(_refreshExchangeRates);
@@ -251,6 +257,9 @@ class FlowState extends State {
LocalPreferences().primaryCurrency.removeListener(_refreshExchangeRates);
UserPreferencesService().valueNotifier.removeListener(_reloadTheme);
UserPreferencesService().valueNotifier.removeListener(_listenToShakes);
+ UserPreferencesService().valueNotifier.removeListener(_syncWidgets);
+
+ ExchangeRatesService().exchangeRatesCache.removeListener(_syncWidgets);
TransactionsService().removeListener(_synchronizePlannedNotifications);
@@ -426,6 +435,10 @@ class FlowState extends State {
);
}
+ void _syncWidgets() {
+ WidgetSummarySync.sync();
+ }
+
void _synchronizePlannedNotifications() {
TransactionsService().synchronizeNotifications().catchError((error) {
startupLog.severe("Failed to synchronize notifications", error);
diff --git a/lib/routes/home/stats_tab.dart b/lib/routes/home/stats_tab.dart
index c2bddc8a..f31eb8f0 100644
--- a/lib/routes/home/stats_tab.dart
+++ b/lib/routes/home/stats_tab.dart
@@ -62,11 +62,13 @@ class _StatsTabState extends State
rates = ExchangeRatesService().getPrimaryCurrencyRates();
ExchangeRatesService().exchangeRatesCache.addListener(_updateRates);
+ UserPreferencesService().valueNotifier.addListener(_updateRates);
}
@override
void dispose() {
ExchangeRatesService().exchangeRatesCache.removeListener(_updateRates);
+ UserPreferencesService().valueNotifier.removeListener(_updateRates);
super.dispose();
}
diff --git a/lib/services/user_preferences.dart b/lib/services/user_preferences.dart
index 5dcd8e81..12ca6085 100644
--- a/lib/services/user_preferences.dart
+++ b/lib/services/user_preferences.dart
@@ -16,6 +16,7 @@ import "package:flow/services/currency_registry.dart";
import "package:flow/services/integrations/eny.dart";
import "package:flow/services/notifications.dart";
import "package:flow/services/sync.dart";
+import "package:flow/services/widget_summary_sync.dart";
import "package:flow/theme/color_themes/registry.dart";
import "package:flutter/material.dart";
import "package:home_widget/home_widget.dart";
@@ -464,6 +465,7 @@ class UserPreferencesService {
.then((_) {
ensurePrimaryAccountAvailability();
_updateButtonsWidgets(transactionButtonOrder);
+ WidgetSummarySync.sync();
})
.catchError((e) {
_log.warning("Failed to update widgets button order on init: $e");
diff --git a/lib/services/widget_summary_sync.dart b/lib/services/widget_summary_sync.dart
new file mode 100644
index 00000000..7e200ef0
--- /dev/null
+++ b/lib/services/widget_summary_sync.dart
@@ -0,0 +1,74 @@
+import "dart:io";
+
+import "package:flow/constants.dart";
+import "package:flow/data/exchange_rates.dart";
+import "package:flow/data/single_currency_flow.dart";
+import "package:flow/entity/transaction.dart";
+import "package:flow/l10n/named_enum.dart";
+import "package:flow/objectbox.dart";
+import "package:flow/objectbox/actions.dart";
+import "package:flow/services/exchange_rates.dart";
+import "package:flow/services/user_preferences.dart";
+import "package:home_widget/home_widget.dart";
+import "package:logging/logging.dart";
+import "package:moment_dart/moment_dart.dart";
+
+final Logger _log = Logger("WidgetSummarySync");
+
+class WidgetSummarySync {
+ static Future sync() async {
+ try {
+ final String primaryCurrency =
+ UserPreferencesService().primaryCurrency;
+ final ExchangeRates? rates =
+ ExchangeRatesService().getPrimaryCurrencyRates();
+
+ final TimeRange range = TimeRange.thisMonth();
+
+ final List transactions = await ObjectBox()
+ .transcationsByRange(range, includeTransfers: false);
+
+ final now = DateTime.now();
+
+ final SingleCurrencyFlow flow =
+ SingleCurrencyFlow(currency: primaryCurrency)
+ ..addAll(
+ transactions
+ .where((t) => !t.transactionDate.isAfter(now))
+ .where((t) => t.isPending != true)
+ .map((t) => t.money),
+ rates,
+ );
+
+ final String formattedIncome =
+ flow.totalIncome.formatMoney(compact: true);
+ final String formattedExpense =
+ flow.totalExpense.abs().formatMoney(compact: true);
+
+ final String incomeLabel = TransactionType.income.localizedName;
+ final String expenseLabel = TransactionType.expense.localizedName;
+
+ if (Platform.isIOS) {
+ await HomeWidget.setAppGroupId(iOSAppGroupId);
+ }
+
+ await HomeWidget.saveWidgetData("summaryIncome", formattedIncome);
+ await HomeWidget.saveWidgetData("summaryExpense", formattedExpense);
+ await HomeWidget.saveWidgetData("summaryIncomeLabel", incomeLabel);
+ await HomeWidget.saveWidgetData("summaryExpenseLabel", expenseLabel);
+
+ await HomeWidget.updateWidget(
+ name: "FlowSummaryWidget",
+ iOSName: "FlowSummaryWidget",
+ androidName: "SummaryReceiver",
+ qualifiedAndroidName: "mn.flow.flow.glance.SummaryReceiver",
+ );
+
+ _log.finest(
+ "Synced summary widget: income=$formattedIncome, expense=$formattedExpense",
+ );
+ } catch (e) {
+ _log.warning("Failed to sync summary widget: $e");
+ }
+ }
+}
diff --git a/lib/widgets/home/home/account/total_balance.dart b/lib/widgets/home/home/account/total_balance.dart
index 15f33d24..9b193392 100644
--- a/lib/widgets/home/home/account/total_balance.dart
+++ b/lib/widgets/home/home/account/total_balance.dart
@@ -5,6 +5,7 @@ import "package:flow/objectbox/actions.dart";
import "package:flow/prefs/local_preferences.dart";
import "package:flow/services/exchange_rates.dart";
import "package:flow/services/transactions.dart";
+import "package:flow/services/user_preferences.dart";
import "package:flow/theme/theme.dart";
import "package:flow/widgets/general/money_text.dart";
import "package:flutter/material.dart";
@@ -25,6 +26,7 @@ class _TotalBalanceState extends State {
void initState() {
super.initState();
LocalPreferences().primaryCurrency.addListener(_refresh);
+ UserPreferencesService().valueNotifier.addListener(_refresh);
ExchangeRatesService().exchangeRatesCache.addListener(_refresh);
TransactionsService().addListener(_refresh);
@@ -37,6 +39,7 @@ class _TotalBalanceState extends State {
@override
void dispose() {
LocalPreferences().primaryCurrency.removeListener(_refresh);
+ UserPreferencesService().valueNotifier.removeListener(_refresh);
ExchangeRatesService().exchangeRatesCache.removeListener(_refresh);
TransactionsService().removeListener(_refresh);
diff --git a/lib/widgets/home/stats/most_spending_category.dart b/lib/widgets/home/stats/most_spending_category.dart
index fe9c4635..a158b585 100644
--- a/lib/widgets/home/stats/most_spending_category.dart
+++ b/lib/widgets/home/stats/most_spending_category.dart
@@ -47,6 +47,13 @@ class _MostSpendingCategoryState extends State {
super.initState();
range = widget.range;
fetch();
+ UserPreferencesService().valueNotifier.addListener(fetch);
+ }
+
+ @override
+ void dispose() {
+ UserPreferencesService().valueNotifier.removeListener(fetch);
+ super.dispose();
}
@override
diff --git a/pubspec.yaml b/pubspec.yaml
index fa84f649..49be24f2 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.20.0+338"
+version: "0.21.0+339"
environment:
sdk: ">=3.10.0 <4.0.0"