/// 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 { TransactionFilterSelection? filter = initialFilter ?? existing?.filter; if (!skipFilterDialog || filter == null) { final bank = context.read(); final meta = context.read(); filter = await showTransactionFilterDialog( context, available: meta.allLabels, transactions: bank.transactions, initial: filter ?? const TransactionFilterSelection(labelIds: {}), ); if (filter == null || !context.mounted) return null; } final name = await promptFavouriteName( context, initial: existing?.name ?? '', title: existing == null ? 'Name this widget' : 'Rename widget', ); if (name == null || !context.mounted) return null; await Future.delayed(Duration.zero); if (!context.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(context, draft); } Future _promptWidgetMetrics( BuildContext context, HomeFilterWidget draft, ) { var showIn = draft.showAmountIn; var showOut = draft.showAmountOut; var showCount = draft.showCount; return showDialog( context: context, useRootNavigator: true, builder: (context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: const Text('Widget metrics'), 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), ), ], ), 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, ), ); }, 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 widgets', style: theme.textTheme.titleLarge), const SizedBox(height: 4), Text( 'Pin saved transaction filters on Home. Check Display to ' 'show a widget; edit metrics or remove anytime.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: OutlinedButton.icon( onPressed: () async { final created = await composeHomeFilterWidget(context); if (created == null || !context.mounted) return; await prefs.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 widgets yet. Add a filter, or save one 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: IconButton( tooltip: 'Edit', onPressed: () async { final updated = await composeHomeFilterWidget( context, existing: widgetItem, ); if (updated == null || !context.mounted) return; await prefs.updateHomeFilterWidget(updated); }, icon: const Icon(Icons.edit_outlined), ), 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', ]; return parts.isEmpty ? 'All transactions' : parts.join(' · '); } }