208 lines
6.3 KiB
Dart
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'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|