Files
ingtrakapp/lib/widgets/label_parent_picker.dart
T
2026-09-08 23:52:54 +02:00

208 lines
6.3 KiB
Dart

/// 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<void> showMoveLabelDialog(
BuildContext context, {
required TxLabel label,
}) async {
final meta = context.read<TxMetaController>();
final parents = meta.validParentsFor(label.id);
final currentParent = meta.parentIdOf(label.id);
final chosen = await showDialog<String?>(
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<TxLabel> 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<TxLabel> 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<TxLabel>.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<String?>(
initialValue: _parentId,
decoration: const InputDecoration(
labelText: 'Under label (optional)',
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('Top level'),
),
for (final parent in parentChoices)
DropdownMenuItem<String?>(
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'),
),
],
);
}
}