nice overview working
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/// 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user