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],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user