spinner feedback to ui
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user