new widget types

This commit is contained in:
davmar
2026-09-10 17:31:18 +02:00
parent 2727f1baf2
commit 86b5f1e09b
8 changed files with 817 additions and 89 deletions
+231 -37
View File
@@ -80,10 +80,23 @@ Future<HomeFilterWidget?> _promptWidgetMetrics(
BuildContext context,
HomeFilterWidget draft,
) {
var type = draft.type;
var iconId = draft.iconId;
var chartStyle = draft.chartStyle;
var budgetCapMode = draft.budgetCapMode;
var budgetFixedAmount = draft.budgetFixedAmount;
var budgetAverageMonths = draft.budgetAverageMonths;
var showIn = draft.showAmountIn;
var showOut = draft.showAmountOut;
var showCount = draft.showCount;
var halfWidth = draft.halfWidth;
final amountController = TextEditingController(
text: budgetFixedAmount > 0
? (budgetFixedAmount == budgetFixedAmount.roundToDouble()
? budgetFixedAmount.toStringAsFixed(0)
: budgetFixedAmount.toStringAsFixed(2))
: '',
);
return showDialog<HomeFilterWidget>(
context: context,
@@ -91,40 +104,195 @@ Future<HomeFilterWidget?> _promptWidgetMetrics(
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
final theme = Theme.of(context);
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),
content: SizedBox(
width: 360,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Type', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
SegmentedButton<HomeWidgetType>(
segments: const [
ButtonSegment(
value: HomeWidgetType.stats,
label: Text('Stats'),
icon: Icon(Icons.analytics_outlined),
),
ButtonSegment(
value: HomeWidgetType.budget,
label: Text('Budget'),
icon: Icon(Icons.savings_outlined),
),
],
selected: {type},
onSelectionChanged: (value) {
setState(() => type = value.first);
},
),
const SizedBox(height: 16),
Text('Icon', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
for (final entry in HomeWidgetIcons.all)
ChoiceChip(
label: Icon(entry.$2, size: 18),
selected: iconId == entry.$1,
onSelected: (_) =>
setState(() => iconId = entry.$1),
tooltip: entry.$3,
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
),
],
),
const SizedBox(height: 12),
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),
),
if (type == HomeWidgetType.stats) ...[
const Divider(),
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),
),
] else ...[
const Divider(),
Text('Budget cap', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
SegmentedButton<HomeBudgetCapMode>(
segments: const [
ButtonSegment(
value: HomeBudgetCapMode.fixed,
label: Text('Fixed'),
),
ButtonSegment(
value: HomeBudgetCapMode.averageMonths,
label: Text('Average'),
),
],
selected: {budgetCapMode},
onSelectionChanged: (value) {
setState(() => budgetCapMode = value.first);
},
),
const SizedBox(height: 12),
if (budgetCapMode == HomeBudgetCapMode.fixed)
TextField(
controller: amountController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(
labelText: 'Budget amount',
hintText: 'e.g. 500',
border: OutlineInputBorder(),
isDense: true,
),
)
else
Row(
children: [
Expanded(
child: Text(
'Average |net| of last',
style: theme.textTheme.bodyMedium,
),
),
SizedBox(
width: 88,
child: DropdownButtonFormField<int>(
initialValue: budgetAverageMonths,
decoration: const InputDecoration(
border: OutlineInputBorder(),
isDense: true,
suffixText: 'mo',
),
items: [
for (final n in const [1, 2, 3, 6, 12, 24])
DropdownMenuItem(
value: n,
child: Text('$n'),
),
],
onChanged: (value) {
if (value == null) return;
setState(
() => budgetAverageMonths = value,
);
},
),
),
],
),
const SizedBox(height: 16),
Text('Chart', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
SegmentedButton<HomeWidgetChartStyle>(
segments: const [
ButtonSegment(
value: HomeWidgetChartStyle.bar,
label: Text('Bar'),
icon: Icon(Icons.linear_scale),
),
ButtonSegment(
value: HomeWidgetChartStyle.pie,
label: Text('Pie'),
icon: Icon(Icons.pie_chart_outline),
),
ButtonSegment(
value: HomeWidgetChartStyle.ring,
label: Text('Ring'),
icon: Icon(Icons.donut_large),
),
],
selected: {chartStyle},
onSelectionChanged: (value) {
setState(() => chartStyle = value.first);
},
),
const SizedBox(height: 8),
Text(
'Progress uses this month’s net (|in − out|) for the '
'filter toward the cap.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
],
),
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(
@@ -133,9 +301,29 @@ Future<HomeFilterWidget?> _promptWidgetMetrics(
),
FilledButton(
onPressed: () {
if (!showIn && !showOut && !showCount) return;
if (type == HomeWidgetType.stats &&
!showIn &&
!showOut &&
!showCount) {
return;
}
var fixed = budgetFixedAmount;
if (type == HomeWidgetType.budget &&
budgetCapMode == HomeBudgetCapMode.fixed) {
final parsed = double.tryParse(
amountController.text.trim().replaceAll(',', '.'),
);
if (parsed == null || parsed <= 0) return;
fixed = parsed;
}
Navigator.of(context).pop(
draft.copyWith(
type: type,
iconId: iconId,
chartStyle: chartStyle,
budgetCapMode: budgetCapMode,
budgetFixedAmount: fixed,
budgetAverageMonths: budgetAverageMonths,
showAmountIn: showIn,
showAmountOut: showOut,
showCount: showCount,
@@ -150,7 +338,7 @@ Future<HomeFilterWidget?> _promptWidgetMetrics(
},
);
},
);
).whenComplete(amountController.dispose);
}
class _HomeWidgetsSheet extends StatelessWidget {
@@ -318,15 +506,21 @@ class _HomeWidgetsSheet extends StatelessWidget {
static String _subtitle(HomeFilterWidget w) {
final parts = <String>[
w.type == HomeWidgetType.budget ? 'Budget' : 'Stats',
if (w.type == HomeWidgetType.budget)
w.budgetCapMode == HomeBudgetCapMode.fixed
? 'Fixed'
: 'Avg ${w.budgetAverageMonths}mo',
if (w.type == HomeWidgetType.budget) w.chartStyle.name,
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.type == HomeWidgetType.stats && w.showAmountIn) 'In',
if (w.type == HomeWidgetType.stats && w.showAmountOut) 'Out',
if (w.type == HomeWidgetType.stats && w.showCount) 'Count',
if (w.halfWidth) 'Half',
];
return parts.isEmpty ? 'All transactions' : parts.join(' · ');
return parts.join(' · ');
}
}