/// Manage Home filter widgets: display, metrics, add/remove. library; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/home_filter_widget.dart'; import '../models/transaction_meta.dart'; import '../providers/bank_provider.dart'; import '../providers/preferences_provider.dart'; import '../providers/tx_meta_provider.dart'; import '../widgets/label_filter_dialog.dart'; import '../widgets/overview_favourites_sheet.dart' show promptFavouriteName; Future showHomeWidgetsSheet(BuildContext context) { return showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, builder: (context) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.viewInsetsOf(context).bottom, ), child: const _HomeWidgetsSheet(), ); }, ); } Future composeHomeFilterWidget( BuildContext context, { TransactionFilterSelection? initialFilter, HomeFilterWidget? existing, bool skipFilterDialog = false, }) async { // Capture before any modal opens โ€” the caller's element may be rebuilt. final bank = context.read(); final meta = context.read(); final navigator = Navigator.of(context); TransactionFilterSelection? filter = initialFilter ?? existing?.filter; if (!skipFilterDialog || filter == null) { filter = await showTransactionFilterDialog( navigator.context, available: meta.allLabels, transactions: bank.transactions, initial: filter ?? const TransactionFilterSelection(labelIds: {}), ); if (filter == null || !navigator.mounted) return null; } if (!navigator.mounted) return null; final name = await promptFavouriteName( navigator.context, initial: existing?.name ?? '', title: existing == null ? 'Name this widget' : 'Rename widget', ); if (name == null || !navigator.mounted) return null; // Let the name dialog finish popping before presenting options. await WidgetsBinding.instance.endOfFrame; if (!navigator.mounted) return null; final draft = existing == null ? HomeFilterWidget.fromFilter(name: name, filter: filter) : existing.copyWith( name: name, labelIds: filter.labelIds.toList(growable: false), flow: filter.flow, amountRange: filter.amountRange, query: filter.query, monthOnly: filter.monthOnly, ); return _promptWidgetMetrics(navigator.context, draft); } Future _promptWidgetMetrics( BuildContext context, HomeFilterWidget draft, ) { var showIn = draft.showAmountIn; var showOut = draft.showAmountOut; var showCount = draft.showCount; var halfWidth = draft.halfWidth; return showDialog( context: context, useRootNavigator: true, builder: (context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: const Text('Widget options'), content: Column( mainAxisSize: MainAxisSize.min, children: [ CheckboxListTile( contentPadding: EdgeInsets.zero, title: const Text('Amount in'), value: showIn, onChanged: (value) => setState(() => showIn = value ?? false), ), CheckboxListTile( contentPadding: EdgeInsets.zero, title: const Text('Amount out'), value: showOut, onChanged: (value) => setState(() => showOut = value ?? false), ), CheckboxListTile( contentPadding: EdgeInsets.zero, title: const Text('Number of transactions'), value: showCount, onChanged: (value) => setState(() => showCount = value ?? false), ), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Half width'), subtitle: const Text('Share the row with another widget'), value: halfWidth, onChanged: (value) => setState(() => halfWidth = value), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { if (!showIn && !showOut && !showCount) return; Navigator.of(context).pop( draft.copyWith( showAmountIn: showIn, showAmountOut: showOut, showCount: showCount, halfWidth: halfWidth, ), ); }, child: const Text('Save'), ), ], ); }, ); }, ); } class _HomeWidgetsSheet extends StatelessWidget { const _HomeWidgetsSheet(); @override Widget build(BuildContext context) { final prefs = context.watch(); final theme = Theme.of(context); final widgets = prefs.homeFilterWidgets; return SafeArea( child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.sizeOf(context).height * 0.72, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 4, 20, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Home layout', style: theme.textTheme.titleLarge), const SizedBox(height: 4), Text( 'Choose which built-in sections and filter widgets appear ' 'on Home. Check Display to show an item.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), child: Column( children: [ CheckboxListTile( value: prefs.homeBuiltInSections.showTransactions, onChanged: (value) { if (value == null) return; prefs.setHomeShowTransactions(value); }, title: const Text('Transactions'), subtitle: const Text('Total transaction count'), controlAffinity: ListTileControlAffinity.leading, ), CheckboxListTile( value: prefs.homeBuiltInSections.showCategorized, onChanged: (value) { if (value == null) return; prefs.setHomeShowCategorized(value); }, title: const Text('Categorized'), subtitle: const Text('Labeled transaction count'), controlAffinity: ListTileControlAffinity.leading, ), CheckboxListTile( value: prefs.homeBuiltInSections.showRecent, onChanged: (value) { if (value == null) return; prefs.setHomeShowRecent(value); }, title: const Text('Recent transactions'), subtitle: const Text('Latest activity list'), controlAffinity: ListTileControlAffinity.leading, ), ], ), ), const Divider(height: 1), Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: OutlinedButton.icon( onPressed: () async { final preferences = context.read(); final created = await composeHomeFilterWidget(context); if (created == null) return; await preferences.addHomeFilterWidget(created); }, icon: const Icon(Icons.add), label: const Text('Add filter widget'), ), ), const Divider(height: 1), Expanded( child: widgets.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'No filter widgets yet. Add one here, or save a ' 'filter from Transactions.', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ) : ListView.separated( padding: const EdgeInsets.fromLTRB(8, 8, 8, 24), itemCount: widgets.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final widgetItem = widgets[index]; return CheckboxListTile( value: widgetItem.display, onChanged: (value) { if (value == null) return; prefs.setHomeFilterWidgetDisplay( widgetItem.id, value, ); }, secondary: PopupMenuButton( tooltip: 'Widget actions', onSelected: (value) async { if (value == 'edit') { final preferences = context.read(); final updated = await composeHomeFilterWidget( context, existing: widgetItem, ); if (updated == null) return; await preferences .updateHomeFilterWidget(updated); } else if (value == 'delete') { await prefs .removeHomeFilterWidget(widgetItem.id); } }, itemBuilder: (context) => const [ PopupMenuItem( value: 'edit', child: Text('Edit'), ), PopupMenuItem( value: 'delete', child: Text('Delete'), ), ], ), title: Text(widgetItem.name), subtitle: Text( '${_subtitle(widgetItem)}\n' 'Display on Home', ), isThreeLine: true, controlAffinity: ListTileControlAffinity.leading, ); }, ), ), ], ), ), ); } static String _subtitle(HomeFilterWidget w) { final parts = [ if (w.monthOnly) 'This month', if (w.flow != TxFlowFilter.all) w.flow.name, if (w.labelIds.isNotEmpty) '${w.labelIds.length} labels', if (w.query.trim().isNotEmpty) 'โ€œ${w.query.trim()}โ€', if (w.showAmountIn) 'In', if (w.showAmountOut) 'Out', if (w.showCount) 'Count', if (w.halfWidth) 'Half', ]; return parts.isEmpty ? 'All transactions' : parts.join(' ยท '); } }