Files
ingtrakapp/lib/screens/labels_screen.dart
2026-09-08 23:52:54 +02:00

202 lines
7.3 KiB
Dart

/// Manage labels: create, nest, and delete.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/transaction_meta.dart';
import '../providers/tx_meta_provider.dart';
import '../widgets/label_parent_picker.dart';
import '../widgets/responsive_body.dart';
class LabelsScreen extends StatelessWidget {
const LabelsScreen({super.key});
Future<void> _create(
BuildContext context, {
String? parentId,
}) async {
final meta = context.read<TxMetaController>();
final result = await showCreateLabelDialog(
context,
parents: meta.allLabels,
initialParentId: parentId,
);
if (result == null || !context.mounted) return;
await meta.createLabel(result.name, parentId: result.parentId);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Created “${result.name}”')),
);
}
Future<void> _delete(BuildContext context, TxLabel label) async {
final ok = await showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text('Remove “${label.name}”?'),
content: const Text(
'This deletes the label. Transactions keep any other labels. '
'Sub-labels move up one level.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: const Text('Remove'),
),
],
);
},
);
if (ok != true || !context.mounted) return;
await context.read<TxMetaController>().removeLabel(label.id);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Removed “${label.name}”')),
);
}
@override
Widget build(BuildContext context) {
final meta = context.watch<TxMetaController>();
final theme = Theme.of(context);
final colors = theme.colorScheme;
final entries = meta.labelsInTreeOrder();
return Scaffold(
appBar: AppBar(
title: const Text('Labels'),
actions: [
IconButton(
tooltip: 'New label',
onPressed: () => _create(context),
icon: const Icon(Icons.add),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _create(context),
icon: const Icon(Icons.add),
label: const Text('New label'),
),
body: ResponsiveBody(
child: entries.isEmpty
? ListView(
children: [
const SizedBox(height: 48),
Icon(
Icons.label_outline,
size: 48,
color: colors.outline,
),
const SizedBox(height: 12),
Text(
'No labels yet',
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'Create labels to categorize spending.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
],
)
: ListView.separated(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 88),
itemCount: entries.length + 1,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 12),
child: Text(
'Create, nest under another label, or delete custom '
'ones. Built-in labels can be moved but not deleted.',
style: theme.textTheme.bodyMedium,
),
);
}
final entry = entries[index - 1];
final label = entry.label;
final parentId = meta.parentIdOf(label.id);
final parent = parentId == null ? null : meta.labelById(parentId);
return ListTile(
contentPadding: EdgeInsets.fromLTRB(
8 + entry.depth * 16.0,
0,
4,
0,
),
leading: Icon(label.icon, color: label.color),
title: Text(label.name),
subtitle: Text(
[
if (label.custom) 'Custom' else 'Built-in',
if (parent != null) 'under ${parent.name}',
].join(' · '),
),
trailing: PopupMenuButton<String>(
tooltip: 'Actions',
onSelected: (value) async {
switch (value) {
case 'sub':
await _create(context, parentId: label.id);
case 'move':
await showMoveLabelDialog(context, label: label);
case 'delete':
await _delete(context, label);
}
},
itemBuilder: (context) => [
if (meta.depthOf(label.id) < kMaxLabelDepth)
const PopupMenuItem(
value: 'sub',
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.subdirectory_arrow_right),
title: Text('Add sub-label'),
),
),
const PopupMenuItem(
value: 'move',
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.account_tree_outlined),
title: Text('Move under…'),
),
),
if (label.custom)
PopupMenuItem(
value: 'delete',
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(
Icons.delete_outline,
color: colors.error,
),
title: Text(
'Delete',
style: TextStyle(color: colors.error),
),
),
),
],
),
);
},
),
),
);
}
}