From 3e74545bd9a45bb9e4ec0d1f040d5496ac4f3aa9 Mon Sep 17 00:00:00 2001 From: PawiX25 Date: Sun, 19 Apr 2026 04:11:25 +0200 Subject: [PATCH 1/6] fix: TwoEntryLast widget shows last 2 buttons instead of first 2 Fixes #697. The widget was using .subList(0, 2) which returns the same first two buttons as TwoEntry. Changed to .takeLast(2) to correctly show the last two buttons from the user's button order. --- android/app/src/main/kotlin/mn/flow/flow/glance/TwoEntryLast.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) From 34d964b09c57e4f6f36db5079cafff1fc916bb10 Mon Sep 17 00:00:00 2001 From: PawiX25 Date: Sun, 19 Apr 2026 05:28:05 +0200 Subject: [PATCH 2/6] feat: add summary home screen widget (monthly income & expenses) - New Dart sync service (WidgetSummarySync) calculates current month income/expense totals and pushes to native widgets via home_widget - Android: Jetpack Glance widget with Material You dynamic theming, exact Material Symbols icon glyphs, horizontal-only resize - iOS: WidgetKit SwiftUI widget (.systemSmall + .systemMedium) using existing asset catalog icons with template rendering - Reactive to: transaction changes, currency changes, exchange rate updates, and locale changes - Filters out future and pending transactions - Expense shown as absolute value (no minus sign) - Also fixes currency change reactivity in StatsTab, TotalBalance, and MostSpendingCategory widgets Closes #699 --- CHANGELOG.md | 6 + android/app/src/main/AndroidManifest.xml | 10 ++ .../kotlin/mn/flow/flow/glance/Summary.kt | 144 ++++++++++++++++++ .../mn/flow/flow/glance/SummaryReceiver.kt | 8 + .../res/drawable/ic_arrow_down_forward.xml | 9 ++ .../main/res/drawable/ic_arrow_up_forward.xml | 9 ++ .../main/res/values-night/widget_colors.xml | 5 + .../app/src/main/res/values/widget_colors.xml | 5 + .../src/main/res/xml/summary_widget_info.xml | 10 ++ ios/Flow Widgets/LeBundle.swift | 1 + ios/Flow Widgets/SummaryWidget.swift | 109 +++++++++++++ lib/l10n/flow_localizations.dart | 2 + lib/main.dart | 13 ++ lib/routes/home/stats_tab.dart | 2 + lib/services/user_preferences.dart | 2 + lib/services/widget_summary_sync.dart | 74 +++++++++ .../home/home/account/total_balance.dart | 3 + .../home/stats/most_spending_category.dart | 7 + 18 files changed, 419 insertions(+) create mode 100644 android/app/src/main/kotlin/mn/flow/flow/glance/Summary.kt create mode 100644 android/app/src/main/kotlin/mn/flow/flow/glance/SummaryReceiver.kt create mode 100644 android/app/src/main/res/drawable/ic_arrow_down_forward.xml create mode 100644 android/app/src/main/res/drawable/ic_arrow_up_forward.xml create mode 100644 android/app/src/main/res/values-night/widget_colors.xml create mode 100644 android/app/src/main/res/values/widget_colors.xml create mode 100644 android/app/src/main/res/xml/summary_widget_info.xml create mode 100644 ios/Flow Widgets/SummaryWidget.swift create mode 100644 lib/services/widget_summary_sync.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 0647f30f..22349e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## next + +### New features + +* Added summary home screen widget showing monthly income and expenses (iOS + Android) + ## 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/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..f4394f50 100644 --- a/lib/l10n/flow_localizations.dart +++ b/lib/l10n/flow_localizations.dart @@ -1,6 +1,7 @@ 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 +137,7 @@ class _FlowLocalizationDelegate : FlowLocalizations.supportedLocales[1], ); await localization.load(); + WidgetSummarySync.sync(); 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 From 4883281062569d3be85f44add3136b3f7eabd4fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D1=82=D0=BC=D1=8D=D0=BD=D0=B4=20=D0=93=D0=B0?= =?UTF-8?q?=D0=BD=D0=B1=D0=B0=D0=B0=D1=82=D0=B0=D1=80?= Date: Sun, 19 Apr 2026 16:57:39 +0800 Subject: [PATCH 3/6] "Claude PR Assistant workflow" --- .github/workflows/claude.yml | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/claude.yml 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 *)' + From 12904f795a7140be2599df764a1ecc81d97ad5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D1=82=D0=BC=D1=8D=D0=BD=D0=B4=20=D0=93=D0=B0?= =?UTF-8?q?=D0=BD=D0=B1=D0=B0=D0=B0=D1=82=D0=B0=D1=80?= Date: Sun, 19 Apr 2026 16:57:41 +0800 Subject: [PATCH 4/6] "Claude Code Review workflow" --- .github/workflows/claude-code-review.yml | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml 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 + From d5e25b509dd8d05930f87c0ee0a0c2f88374365f Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 19 Apr 2026 17:09:38 +0800 Subject: [PATCH 5/6] bump version --- CHANGELOG.md | 8 ++++++-- lib/l10n/flow_localizations.dart | 3 ++- pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22349e24..1f4893dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Changelog -## next +## 0.21.0 ### New features -* Added summary home screen widget showing monthly income and expenses (iOS + Android) +* 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 diff --git a/lib/l10n/flow_localizations.dart b/lib/l10n/flow_localizations.dart index f4394f50..d6047d34 100644 --- a/lib/l10n/flow_localizations.dart +++ b/lib/l10n/flow_localizations.dart @@ -1,3 +1,4 @@ +import "dart:async"; import "dart:convert"; import "package:flow/l10n/supported_languages.dart"; @@ -137,7 +138,7 @@ class _FlowLocalizationDelegate : FlowLocalizations.supportedLocales[1], ); await localization.load(); - WidgetSummarySync.sync(); + unawaited(WidgetSummarySync.sync().catchError((_) {})); return localization; } 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" From 1e9c426b28a7251fcc3cfd89599e9acc0b8759df Mon Sep 17 00:00:00 2001 From: Batmend Ganbaatar Date: Sun, 19 Apr 2026 17:10:03 +0800 Subject: [PATCH 6/6] bump version --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4893dc..6278555b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 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)