spinner feedback to ui
This commit is contained in:
+139
-12
@@ -10,8 +10,10 @@ import '../providers/preferences_provider.dart';
|
||||
import '../providers/tx_meta_provider.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
import '../utils/connect_bank.dart';
|
||||
import '../utils/deferred_tab_work.dart';
|
||||
import '../utils/helpers.dart';
|
||||
import '../utils/navigation.dart';
|
||||
import '../utils/tx_filter_metrics.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/home_filter_widget_card.dart';
|
||||
import '../widgets/home_widgets_sheet.dart';
|
||||
@@ -22,6 +24,18 @@ import '../widgets/skip_build_when_covered.dart';
|
||||
import 'transaction_detail_screen.dart';
|
||||
import 'transactions_screen.dart';
|
||||
|
||||
class _HomeSnapshot {
|
||||
const _HomeSnapshot({
|
||||
required this.categorized,
|
||||
required this.statsById,
|
||||
required this.budgetById,
|
||||
});
|
||||
|
||||
final int categorized;
|
||||
final Map<String, TxFilterMetrics> statsById;
|
||||
final Map<String, HomeBudgetMetrics> budgetById;
|
||||
}
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@@ -33,16 +47,93 @@ class HomeScreen extends StatefulWidget {
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
var _editingWidgets = false;
|
||||
final _work = DeferredTabWork<_HomeSnapshot>();
|
||||
|
||||
int _homeCacheKey({
|
||||
required TxMetaController meta,
|
||||
required BankController bank,
|
||||
required List<HomeFilterWidget> widgets,
|
||||
}) {
|
||||
return Object.hash(
|
||||
meta.storeRevision,
|
||||
identityHashCode(bank.transactions),
|
||||
Object.hashAll(widgets.map((w) => w.id)),
|
||||
Object.hashAll(widgets.map((w) => w.updatedAt)),
|
||||
_editingWidgets,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Inactive IndexedStack tabs must not watch providers — otherwise every
|
||||
// annotation save rebuilds Home while the user is on another tab.
|
||||
if (ShellTabScope.of(context) != HomeScreen.tabIndex) {
|
||||
final active = ShellTabScope.of(context) == HomeScreen.tabIndex;
|
||||
if (!active) {
|
||||
if (_work.isLoading) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (ShellTabScope.of(context) == HomeScreen.tabIndex) return;
|
||||
_work.cancel();
|
||||
});
|
||||
}
|
||||
return const SizedBox.expand();
|
||||
}
|
||||
|
||||
final bank = context.watch<BankController>();
|
||||
final meta = context.watch<TxMetaController>();
|
||||
final prefs = context.watch<PreferencesController>();
|
||||
final widgets = _editingWidgets
|
||||
? prefs.homeFilterWidgets
|
||||
: prefs.displayedHomeFilterWidgets;
|
||||
final needsData = bank.isLinked || bank.hasData;
|
||||
final emptyLoading = bank.isLoading && bank.balance == null && !bank.hasData;
|
||||
final cacheKey = _homeCacheKey(meta: meta, bank: bank, widgets: widgets);
|
||||
|
||||
if (needsData && !emptyLoading && !_work.readyFor(cacheKey)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (ShellTabScope.of(context) != HomeScreen.tabIndex) return;
|
||||
_work.ensure(
|
||||
key: cacheKey,
|
||||
mounted: () => mounted,
|
||||
onUpdate: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
compute: (yieldUi) async {
|
||||
final categorized =
|
||||
meta.labeledTransactionCount(bank.transactions);
|
||||
await yieldUi();
|
||||
final stats = <String, TxFilterMetrics>{};
|
||||
final budgets = <String, HomeBudgetMetrics>{};
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
final w = widgets[i];
|
||||
if (w.type == HomeWidgetType.budget) {
|
||||
budgets[w.id] = computeHomeBudgetMetrics(
|
||||
all: bank.transactions,
|
||||
meta: meta,
|
||||
widget: w,
|
||||
);
|
||||
} else {
|
||||
stats[w.id] = computeTxFilterMetrics(
|
||||
all: bank.transactions,
|
||||
meta: meta,
|
||||
filter: w.filter,
|
||||
);
|
||||
}
|
||||
if (i % 2 == 1) await yieldUi();
|
||||
}
|
||||
return _HomeSnapshot(
|
||||
categorized: categorized,
|
||||
statsById: stats,
|
||||
budgetById: budgets,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final showSpinner =
|
||||
needsData && !emptyLoading && !_work.readyFor(cacheKey);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -54,7 +145,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
onPressed: () =>
|
||||
setState(() => _editingWidgets = !_editingWidgets),
|
||||
icon: Icon(
|
||||
_editingWidgets ? Icons.check : Icons.dashboard_customize_outlined,
|
||||
_editingWidgets
|
||||
? Icons.check
|
||||
: Icons.dashboard_customize_outlined,
|
||||
),
|
||||
),
|
||||
...bankAppBarActions(
|
||||
@@ -67,10 +160,19 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
body: RefreshIndicator(
|
||||
onRefresh: bank.refresh,
|
||||
child: ResponsiveBody(
|
||||
child: _HomeBody(
|
||||
bank: bank,
|
||||
editingWidgets: _editingWidgets,
|
||||
),
|
||||
child: showSpinner
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
LoadingView(message: 'Loading home…'),
|
||||
],
|
||||
)
|
||||
: _HomeBody(
|
||||
bank: bank,
|
||||
editingWidgets: _editingWidgets,
|
||||
snapshot: _work.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -81,10 +183,12 @@ class _HomeBody extends StatelessWidget {
|
||||
const _HomeBody({
|
||||
required this.bank,
|
||||
required this.editingWidgets,
|
||||
required this.snapshot,
|
||||
});
|
||||
|
||||
final BankController bank;
|
||||
final bool editingWidgets;
|
||||
final _HomeSnapshot? snapshot;
|
||||
|
||||
void _openTransactions(BuildContext context) {
|
||||
Navigator.of(context).push(
|
||||
@@ -141,7 +245,7 @@ class _HomeBody extends StatelessWidget {
|
||||
final colors = theme.colorScheme;
|
||||
final meta = context.watch<TxMetaController>();
|
||||
final recent = bank.transactions.take(5).toList(growable: false);
|
||||
final categorized =
|
||||
final categorized = snapshot?.categorized ??
|
||||
meta.labeledTransactionCount(bank.transactions);
|
||||
final widgets = editingWidgets
|
||||
? prefs.homeFilterWidgets
|
||||
@@ -319,6 +423,9 @@ class _HomeBody extends StatelessWidget {
|
||||
child: HomeFilterWidgetCard(
|
||||
widgetConfig: widgetConfig,
|
||||
editing: true,
|
||||
precomputedStats: snapshot?.statsById[widgetConfig.id],
|
||||
precomputedBudget:
|
||||
snapshot?.budgetById[widgetConfig.id],
|
||||
onToggleDisplay: (display) =>
|
||||
prefs.setHomeFilterWidgetDisplay(
|
||||
widgetConfig.id,
|
||||
@@ -347,7 +454,11 @@ class _HomeBody extends StatelessWidget {
|
||||
},
|
||||
)
|
||||
else
|
||||
..._packedHomeWidgetRows(widgets),
|
||||
..._packedHomeWidgetRows(
|
||||
widgets,
|
||||
statsById: snapshot?.statsById,
|
||||
budgetById: snapshot?.budgetById,
|
||||
),
|
||||
],
|
||||
if (_showBuiltIn(
|
||||
editing: editingWidgets,
|
||||
@@ -553,7 +664,11 @@ class _HomeWidgetSlot extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _packedHomeWidgetRows(List<HomeFilterWidget> widgets) {
|
||||
List<Widget> _packedHomeWidgetRows(
|
||||
List<HomeFilterWidget> widgets, {
|
||||
Map<String, TxFilterMetrics>? statsById,
|
||||
Map<String, HomeBudgetMetrics>? budgetById,
|
||||
}) {
|
||||
final rows = <Widget>[];
|
||||
var i = 0;
|
||||
while (i < widgets.length) {
|
||||
@@ -571,11 +686,19 @@ List<Widget> _packedHomeWidgetRows(List<HomeFilterWidget> widgets) {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: HomeFilterWidgetCard(widgetConfig: a),
|
||||
child: HomeFilterWidgetCard(
|
||||
widgetConfig: a,
|
||||
precomputedStats: statsById?[a.id],
|
||||
precomputedBudget: budgetById?[a.id],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: HomeFilterWidgetCard(widgetConfig: b),
|
||||
child: HomeFilterWidgetCard(
|
||||
widgetConfig: b,
|
||||
precomputedStats: statsById?[b.id],
|
||||
precomputedBudget: budgetById?[b.id],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -590,7 +713,11 @@ List<Widget> _packedHomeWidgetRows(List<HomeFilterWidget> widgets) {
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: _HomeWidgetSlot(
|
||||
halfWidth: a.halfWidth,
|
||||
child: HomeFilterWidgetCard(widgetConfig: a),
|
||||
child: HomeFilterWidgetCard(
|
||||
widgetConfig: a,
|
||||
precomputedStats: statsById?[a.id],
|
||||
precomputedBudget: budgetById?[a.id],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/// Look ahead: projected balance from recurring templates through a date.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -16,6 +18,7 @@ import '../utils/look_ahead.dart';
|
||||
import '../utils/navigation.dart';
|
||||
import '../utils/tx_calendar.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/loading_view.dart';
|
||||
import '../widgets/recurring_template_sheet.dart';
|
||||
import '../widgets/responsive_body.dart';
|
||||
import '../widgets/shell_tab_scope.dart';
|
||||
@@ -23,6 +26,13 @@ import '../widgets/skip_build_when_covered.dart';
|
||||
|
||||
enum _PeriodMode { monthly, yearly }
|
||||
|
||||
class _ItemsSnapshot {
|
||||
const _ItemsSnapshot({required this.key, required this.items});
|
||||
|
||||
final int key;
|
||||
final List<RecurringItem> items;
|
||||
}
|
||||
|
||||
class LookAheadScreen extends StatefulWidget {
|
||||
const LookAheadScreen({super.key});
|
||||
|
||||
@@ -35,8 +45,9 @@ class LookAheadScreen extends StatefulWidget {
|
||||
class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
var _period = _PeriodMode.monthly;
|
||||
late DateTime _cursor;
|
||||
List<RecurringItem>? _cachedItems;
|
||||
int? _itemsCacheKey;
|
||||
_ItemsSnapshot? _snapshot;
|
||||
int? _loadingKey;
|
||||
int _computeGen = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -129,9 +140,78 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
int _itemsCacheKey(TxMetaController meta, BankController bank) =>
|
||||
Object.hash(meta.storeRevision, identityHashCode(bank.transactions));
|
||||
|
||||
/// Resolve recurring amounts off the build path, yielding so nav stays live.
|
||||
void _ensureItemsLoaded({
|
||||
required int cacheKey,
|
||||
required TxMetaController meta,
|
||||
required BankController bank,
|
||||
}) {
|
||||
if (_snapshot?.key == cacheKey || _loadingKey == cacheKey) return;
|
||||
_loadingKey = cacheKey;
|
||||
final gen = ++_computeGen;
|
||||
final templates = List<RecurringTemplate>.of(meta.recurringTemplates);
|
||||
final transactions = bank.transactions;
|
||||
|
||||
unawaited(() async {
|
||||
// Let the spinner / tab switch paint before heavy work.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (!mounted || gen != _computeGen) return;
|
||||
|
||||
final items = <RecurringItem>[];
|
||||
for (var i = 0; i < templates.length; i++) {
|
||||
final tpl = templates[i];
|
||||
items.add(
|
||||
RecurringItem(
|
||||
templateId: tpl.id,
|
||||
description: tpl.name,
|
||||
amount: resolveRecurringAmount(
|
||||
template: tpl,
|
||||
transactions: transactions,
|
||||
isInternalTransfer: (tx) => meta.metaFor(tx).internalTransfer,
|
||||
labelIdsFor: meta.labelIdsFor,
|
||||
),
|
||||
currency: tpl.currency,
|
||||
dayOfMonth: tpl.dayOfMonth.clamp(1, 28),
|
||||
ruleSummary: recurringRuleSummary(
|
||||
template: tpl,
|
||||
labelName: (id) => meta.labelById(id)?.name ?? 'label',
|
||||
),
|
||||
sourceTxId: tpl.sourceTxId,
|
||||
labelIds: [
|
||||
if (tpl.labelId != null && tpl.labelId!.isNotEmpty) tpl.labelId!,
|
||||
],
|
||||
),
|
||||
);
|
||||
// Yield often so bottom-nav taps are not starved.
|
||||
if (i % 2 == 1) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (!mounted || gen != _computeGen) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted || gen != _computeGen) return;
|
||||
setState(() {
|
||||
_snapshot = _ItemsSnapshot(key: cacheKey, items: items);
|
||||
if (_loadingKey == cacheKey) _loadingKey = null;
|
||||
});
|
||||
}());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (ShellTabScope.of(context) != LookAheadScreen.tabIndex) {
|
||||
final active = ShellTabScope.of(context) == LookAheadScreen.tabIndex;
|
||||
if (!active) {
|
||||
if (_loadingKey != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (ShellTabScope.of(context) == LookAheadScreen.tabIndex) return;
|
||||
_computeGen++;
|
||||
_loadingKey = null;
|
||||
});
|
||||
}
|
||||
return const SizedBox.expand();
|
||||
}
|
||||
return SkipBuildWhenCovered(
|
||||
@@ -165,22 +245,29 @@ class _LookAheadScreenState extends State<LookAheadScreen> {
|
||||
final currentAmount = balance?.amount ?? 0;
|
||||
final today = _today;
|
||||
final through = _through;
|
||||
final cacheKey = Object.hash(
|
||||
meta.storeRevision,
|
||||
identityHashCode(bank.transactions),
|
||||
);
|
||||
if (_cachedItems == null || _itemsCacheKey != cacheKey) {
|
||||
_itemsCacheKey = cacheKey;
|
||||
_cachedItems = recurringItemsFromTemplates(
|
||||
templates: meta.recurringTemplates,
|
||||
transactions: bank.transactions,
|
||||
isInternalTransfer: (tx) => meta.metaFor(tx).internalTransfer,
|
||||
labelIdsFor: meta.labelIdsFor,
|
||||
labelName: (id) => meta.labelById(id)?.name ?? 'label',
|
||||
final cacheKey = _itemsCacheKey(meta, bank);
|
||||
if (_snapshot?.key != cacheKey && _loadingKey != cacheKey) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (ShellTabScope.of(context) != LookAheadScreen.tabIndex) return;
|
||||
_ensureItemsLoaded(cacheKey: cacheKey, meta: meta, bank: bank);
|
||||
});
|
||||
}
|
||||
|
||||
final snapshot = _snapshot;
|
||||
final ready = snapshot != null && snapshot.key == cacheKey;
|
||||
|
||||
if (!ready) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Look ahead'),
|
||||
actions: bankAppBarActions(context: context),
|
||||
),
|
||||
body: const LoadingView(message: 'Calculating look ahead…'),
|
||||
);
|
||||
}
|
||||
final items = _cachedItems!;
|
||||
|
||||
final items = snapshot.items;
|
||||
final projected = projectedOccurrences(
|
||||
items: items,
|
||||
from: today,
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../providers/bank_provider.dart';
|
||||
import '../providers/preferences_provider.dart';
|
||||
import '../providers/tx_meta_provider.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
import '../utils/deferred_tab_work.dart';
|
||||
import '../utils/helpers.dart';
|
||||
import '../utils/navigation.dart';
|
||||
import '../utils/tx_calendar.dart';
|
||||
@@ -28,6 +29,36 @@ import 'transactions_screen.dart';
|
||||
const _kIncomeColor = AppTheme.accent;
|
||||
const _kExpenseColor = AppTheme.expense;
|
||||
|
||||
class _FavouriteAgg {
|
||||
const _FavouriteAgg({
|
||||
required this.periods,
|
||||
required this.monthly,
|
||||
required this.labelSpend,
|
||||
required this.labelAmountByPeriod,
|
||||
});
|
||||
|
||||
final List<DateTime> periods;
|
||||
final Map<DateTime, _MonthBucket> monthly;
|
||||
final List<_LabelSpend> labelSpend;
|
||||
final Map<DateTime, Map<String, double>> labelAmountByPeriod;
|
||||
}
|
||||
|
||||
class _OverviewAggSnapshot {
|
||||
const _OverviewAggSnapshot({
|
||||
required this.periods,
|
||||
required this.monthly,
|
||||
required this.labelSpend,
|
||||
required this.labelAmountByPeriod,
|
||||
required this.favourites,
|
||||
});
|
||||
|
||||
final List<DateTime> periods;
|
||||
final Map<DateTime, _MonthBucket> monthly;
|
||||
final List<_LabelSpend> labelSpend;
|
||||
final Map<DateTime, Map<String, double>> labelAmountByPeriod;
|
||||
final Map<String, _FavouriteAgg> favourites;
|
||||
}
|
||||
|
||||
class OverviewScreen extends StatefulWidget {
|
||||
const OverviewScreen({super.key});
|
||||
|
||||
@@ -59,6 +90,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
/// Favourites charts view (vs full overview with history).
|
||||
bool _showFavouritesView = false;
|
||||
var _favouritesDefaultApplied = false;
|
||||
final _work = DeferredTabWork<_OverviewAggSnapshot>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -247,7 +279,15 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (ShellTabScope.of(context) != OverviewScreen.tabIndex) {
|
||||
final active = ShellTabScope.of(context) == OverviewScreen.tabIndex;
|
||||
if (!active) {
|
||||
if (_work.isLoading) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (ShellTabScope.of(context) == OverviewScreen.tabIndex) return;
|
||||
_work.cancel();
|
||||
});
|
||||
}
|
||||
return const SizedBox.expand();
|
||||
}
|
||||
|
||||
@@ -268,6 +308,41 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
final emptyLoading =
|
||||
bank.isLoading && bank.transactions.isEmpty && bank.balance == null;
|
||||
final needsData = bank.isLinked || bank.hasData;
|
||||
final displayedFavs = prefs.displayedOverviewFavourites;
|
||||
final cacheKey = Object.hash(
|
||||
meta.storeRevision,
|
||||
identityHashCode(bank.transactions),
|
||||
_showFavouritesView,
|
||||
_period,
|
||||
_metric,
|
||||
_cursor.year,
|
||||
Object.hashAll(_includedLabelIds),
|
||||
_labelFlow,
|
||||
_labelBarStyle,
|
||||
_labelInvertChart,
|
||||
Object.hashAll(displayedFavs.map((f) => f.id)),
|
||||
Object.hashAll(displayedFavs.map((f) => f.updatedAt)),
|
||||
);
|
||||
|
||||
if (needsData && !emptyLoading && !_work.readyFor(cacheKey)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (ShellTabScope.of(context) != OverviewScreen.tabIndex) return;
|
||||
_scheduleOverviewCompute(
|
||||
key: cacheKey,
|
||||
bank: bank,
|
||||
meta: meta,
|
||||
displayedFavs: displayedFavs,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final showSpinner =
|
||||
needsData && !emptyLoading && !_work.readyFor(cacheKey);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_showFavouritesView ? 'Favourites' : 'Overview'),
|
||||
@@ -288,16 +363,107 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
body: RefreshIndicator(
|
||||
onRefresh: bank.refresh,
|
||||
child: ResponsiveBody(
|
||||
child: SkipBuildWhenCovered(
|
||||
builder: (context) => _showFavouritesView
|
||||
? _buildFavouritesBody(context, bank, meta, prefs)
|
||||
: _buildBody(context, bank, meta, prefs),
|
||||
),
|
||||
child: showSpinner
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
LoadingView(message: 'Loading overview…'),
|
||||
],
|
||||
)
|
||||
: SkipBuildWhenCovered(
|
||||
builder: (context) => _showFavouritesView
|
||||
? _buildFavouritesBody(context, bank, meta, prefs)
|
||||
: _buildBody(context, bank, meta, prefs),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _scheduleOverviewCompute({
|
||||
required Object key,
|
||||
required BankController bank,
|
||||
required TxMetaController meta,
|
||||
required List<OverviewFavourite> displayedFavs,
|
||||
}) {
|
||||
_work.ensure(
|
||||
key: key,
|
||||
mounted: () => mounted,
|
||||
onUpdate: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
compute: (yieldUi) async {
|
||||
final txs = bank.transactions;
|
||||
final periods = _chartPeriods(txs);
|
||||
await yieldUi();
|
||||
|
||||
Map<DateTime, _MonthBucket> monthly = const {};
|
||||
var labelSpend = const <_LabelSpend>[];
|
||||
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
|
||||
|
||||
if (!_showFavouritesView) {
|
||||
if (_metric == OverviewMetricMode.flow) {
|
||||
monthly = _flowTotals(txs, meta, periods, _period);
|
||||
} else {
|
||||
final labels = _labelAggregates(
|
||||
txs,
|
||||
meta,
|
||||
periods,
|
||||
_period,
|
||||
includedLabelIds: _includedLabelIds,
|
||||
flow: _labelFlow,
|
||||
);
|
||||
labelSpend = labels.spend;
|
||||
labelAmountByPeriod = labels.amountByPeriod;
|
||||
}
|
||||
await yieldUi();
|
||||
}
|
||||
|
||||
final favourites = <String, _FavouriteAgg>{};
|
||||
if (_showFavouritesView) {
|
||||
for (var i = 0; i < displayedFavs.length; i++) {
|
||||
final favourite = displayedFavs[i];
|
||||
final favPeriods = _chartPeriodsFor(favourite.period, txs);
|
||||
Map<DateTime, _MonthBucket> favMonthly = const {};
|
||||
var favLabels = const <_LabelSpend>[];
|
||||
Map<DateTime, Map<String, double>> favByPeriod = const {};
|
||||
if (favourite.metric == OverviewMetricMode.flow) {
|
||||
favMonthly =
|
||||
_flowTotals(txs, meta, favPeriods, favourite.period);
|
||||
} else {
|
||||
final labels = _labelAggregates(
|
||||
txs,
|
||||
meta,
|
||||
favPeriods,
|
||||
favourite.period,
|
||||
includedLabelIds: favourite.labelIds.toSet(),
|
||||
flow: favourite.flow,
|
||||
);
|
||||
favLabels = labels.spend;
|
||||
favByPeriod = labels.amountByPeriod;
|
||||
}
|
||||
favourites[favourite.id] = _FavouriteAgg(
|
||||
periods: favPeriods,
|
||||
monthly: favMonthly,
|
||||
labelSpend: favLabels,
|
||||
labelAmountByPeriod: favByPeriod,
|
||||
);
|
||||
await yieldUi();
|
||||
}
|
||||
}
|
||||
|
||||
return _OverviewAggSnapshot(
|
||||
periods: periods,
|
||||
monthly: monthly,
|
||||
labelSpend: labelSpend,
|
||||
labelAmountByPeriod: labelAmountByPeriod,
|
||||
favourites: favourites,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFavouritesBody(
|
||||
BuildContext context,
|
||||
BankController bank,
|
||||
@@ -329,6 +495,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
: 'EUR');
|
||||
final txs = bank.transactions;
|
||||
final displayed = prefs.displayedOverviewFavourites;
|
||||
final agg = _work.value;
|
||||
|
||||
return ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
@@ -399,6 +566,7 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
txs: txs,
|
||||
meta: meta,
|
||||
currency: currency,
|
||||
precomputed: agg?.favourites[displayed[i].id],
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -412,25 +580,30 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
required List<BankTransaction> txs,
|
||||
required TxMetaController meta,
|
||||
required String currency,
|
||||
_FavouriteAgg? precomputed,
|
||||
}) {
|
||||
final periods = _chartPeriodsFor(favourite.period, txs);
|
||||
Map<DateTime, _MonthBucket> monthly = const {};
|
||||
var labelSpend = const <_LabelSpend>[];
|
||||
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
|
||||
final periods = precomputed?.periods ??
|
||||
_chartPeriodsFor(favourite.period, txs);
|
||||
Map<DateTime, _MonthBucket> monthly = precomputed?.monthly ?? const {};
|
||||
var labelSpend = precomputed?.labelSpend ?? const <_LabelSpend>[];
|
||||
Map<DateTime, Map<String, double>> labelAmountByPeriod =
|
||||
precomputed?.labelAmountByPeriod ?? const {};
|
||||
|
||||
if (favourite.metric == OverviewMetricMode.flow) {
|
||||
monthly = _flowTotals(txs, meta, periods, favourite.period);
|
||||
} else {
|
||||
final labels = _labelAggregates(
|
||||
txs,
|
||||
meta,
|
||||
periods,
|
||||
favourite.period,
|
||||
includedLabelIds: favourite.labelIds.toSet(),
|
||||
flow: favourite.flow,
|
||||
);
|
||||
labelSpend = labels.spend;
|
||||
labelAmountByPeriod = labels.amountByPeriod;
|
||||
if (precomputed == null) {
|
||||
if (favourite.metric == OverviewMetricMode.flow) {
|
||||
monthly = _flowTotals(txs, meta, periods, favourite.period);
|
||||
} else {
|
||||
final labels = _labelAggregates(
|
||||
txs,
|
||||
meta,
|
||||
periods,
|
||||
favourite.period,
|
||||
includedLabelIds: favourite.labelIds.toSet(),
|
||||
flow: favourite.flow,
|
||||
);
|
||||
labelSpend = labels.spend;
|
||||
labelAmountByPeriod = labels.amountByPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
final periodTitle = favourite.period == OverviewPeriodMode.monthly
|
||||
@@ -530,28 +703,31 @@ class _OverviewScreenState extends State<OverviewScreen> {
|
||||
? bank.transactions.first.currency
|
||||
: 'EUR');
|
||||
final txs = bank.transactions;
|
||||
final periods = _chartPeriods(txs);
|
||||
final agg = _work.value;
|
||||
final periods = agg?.periods ?? _chartPeriods(txs);
|
||||
final historyPeriods = [...periods.reversed];
|
||||
|
||||
// Only aggregate what the active metric needs — toggling was scanning
|
||||
// all txs 3–4 times and resolving full meta each time.
|
||||
Map<DateTime, _MonthBucket> monthly = const {};
|
||||
var labelSpend = const <_LabelSpend>[];
|
||||
Map<DateTime, Map<String, double>> labelAmountByPeriod = const {};
|
||||
// Prefer deferred aggregates; fall back only if snapshot is missing.
|
||||
Map<DateTime, _MonthBucket> monthly = agg?.monthly ?? const {};
|
||||
var labelSpend = agg?.labelSpend ?? const <_LabelSpend>[];
|
||||
Map<DateTime, Map<String, double>> labelAmountByPeriod =
|
||||
agg?.labelAmountByPeriod ?? const {};
|
||||
|
||||
if (_metric == OverviewMetricMode.flow) {
|
||||
monthly = _flowTotals(txs, meta, periods, _period);
|
||||
} else {
|
||||
final labels = _labelAggregates(
|
||||
txs,
|
||||
meta,
|
||||
periods,
|
||||
_period,
|
||||
includedLabelIds: _includedLabelIds,
|
||||
flow: _labelFlow,
|
||||
);
|
||||
labelSpend = labels.spend;
|
||||
labelAmountByPeriod = labels.amountByPeriod;
|
||||
if (agg == null) {
|
||||
if (_metric == OverviewMetricMode.flow) {
|
||||
monthly = _flowTotals(txs, meta, periods, _period);
|
||||
} else {
|
||||
final labels = _labelAggregates(
|
||||
txs,
|
||||
meta,
|
||||
periods,
|
||||
_period,
|
||||
includedLabelIds: _includedLabelIds,
|
||||
flow: _labelFlow,
|
||||
);
|
||||
labelSpend = labels.spend;
|
||||
labelAmountByPeriod = labels.amountByPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
final summaryFlow = _metric == OverviewMetricMode.flow
|
||||
|
||||
Reference in New Issue
Block a user