/// Dialogs to nest or create labels. library; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/transaction_meta.dart'; import '../providers/tx_meta_provider.dart'; Future showMoveLabelDialog( BuildContext context, { required TxLabel label, }) async { final meta = context.read(); final parents = meta.validParentsFor(label.id); final currentParent = meta.parentIdOf(label.id); final chosen = await showDialog( context: context, builder: (dialogContext) { final theme = Theme.of(dialogContext); final maxHeight = MediaQuery.sizeOf(dialogContext).height * 0.45; return AlertDialog( title: Text('Move “${label.name}”'), titleTextStyle: theme.textTheme.titleMedium?.copyWith( color: theme.colorScheme.onSurface, ), contentPadding: const EdgeInsets.fromLTRB(16, 8, 16, 0), content: SizedBox( width: double.maxFinite, child: ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), child: ListView( shrinkWrap: true, padding: EdgeInsets.zero, children: [ ListTile( dense: true, visualDensity: const VisualDensity( horizontal: -4, vertical: -3, ), contentPadding: const EdgeInsets.symmetric(horizontal: 4), minLeadingWidth: 28, leading: Icon( currentParent == null ? Icons.check_circle : Icons.circle_outlined, size: 18, ), title: Text( 'Top level', style: theme.textTheme.bodyMedium, ), onTap: () => Navigator.pop(dialogContext, ''), ), Divider( height: 1, color: theme.colorScheme.outlineVariant.withValues( alpha: 0.5, ), ), for (final parent in parents) ListTile( dense: true, visualDensity: const VisualDensity( horizontal: -4, vertical: -3, ), contentPadding: const EdgeInsets.symmetric(horizontal: 4), minLeadingWidth: 28, leading: Icon( parent.id == currentParent ? Icons.check_circle : parent.icon, size: 18, color: parent.color, ), title: Text( parent.name, style: theme.textTheme.bodyMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), onTap: () => Navigator.pop(dialogContext, parent.id), ), ], ), ), ), actionsPadding: const EdgeInsets.fromLTRB(12, 4, 12, 8), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text('Cancel'), ), ], ); }, ); if (chosen == null || !context.mounted) return; await meta.setLabelParent(label.id, chosen.isEmpty ? null : chosen); } /// Returns name + optional parent, or null if cancelled. Future<({String name, String? parentId})?> showCreateLabelDialog( BuildContext context, { required List parents, String? initialParentId, }) { return showDialog<({String name, String? parentId})>( context: context, builder: (context) => _CreateLabelDialog( parents: parents, initialParentId: initialParentId, ), ); } class _CreateLabelDialog extends StatefulWidget { const _CreateLabelDialog({ required this.parents, this.initialParentId, }); final List parents; final String? initialParentId; @override State<_CreateLabelDialog> createState() => _CreateLabelDialogState(); } class _CreateLabelDialogState extends State<_CreateLabelDialog> { late final TextEditingController _controller; String? _parentId; @override void initState() { super.initState(); _controller = TextEditingController(); _parentId = widget.initialParentId; } @override void dispose() { _controller.dispose(); super.dispose(); } void _submit() { final name = _controller.text.trim(); if (name.isEmpty) return; Navigator.pop(context, (name: name, parentId: _parentId)); } @override Widget build(BuildContext context) { final parentChoices = List.of(widget.parents) ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); return AlertDialog( title: const Text('New label'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _controller, autofocus: true, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration(hintText: 'e.g. Supermarket'), onSubmitted: (_) => _submit(), ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: _parentId, decoration: const InputDecoration( labelText: 'Under label (optional)', ), items: [ const DropdownMenuItem( value: null, child: Text('Top level'), ), for (final parent in parentChoices) DropdownMenuItem( value: parent.id, child: Text(parent.name), ), ], onChanged: (value) => setState(() => _parentId = value), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), FilledButton( onPressed: _submit, child: const Text('Create'), ), ], ); } }