mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-14 21:10:55 +01:00
Wires the shared gate (~/utils/scripts/check_file_length.sh) into a new .pre-commit-config.yaml as a local hook, checking only the files in the commit and failing rather than warning. No baseline, no allowlist. The repo had a hand-written .git/hooks/pre-commit running two checks; both are ported verbatim into the config rather than dropped, with the version bump ordered last so it cannot rewrite pubspec.yaml for a commit a gate is still about to reject. scripts/install_hooks.sh now installs pre-commit (and removes the legacy hook) instead of writing its own. .github/workflows/file-length.yml runs the same check on push, kept separate from release-apk.yml so an over-long file fails lint rather than blocking an APK release. Splits the four cleanest violations, all behaviour-preserving: note_template.dart 370 -> 203 + parser (186) firebase_backend.dart 308 -> barrel (24) + project/store/client port_guard.dart 315 -> 232 + linux_system_probe (100) notes_list_screen.dart 698 -> 225 + 5 widget/nav files Where a file had external callers, the original path stays as a barrel that re-exports the split parts, so no call site changed. Verified: 313 tests pass, coverage identical at 99.9% / 1922 lines (per-file too -- notes_list's 281 covered lines are now 81+75+51+41+22+11), flutter analyze clean, and a staged 260-line file makes git commit fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 lines
4.3 KiB
Dart
153 lines
4.3 KiB
Dart
/// The per-note actions sheet shown from the notes list.
|
|
///
|
|
/// Split out of `notes_list_screen.dart` for file size; public only because
|
|
/// Dart privacy is library-scoped.
|
|
library;
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_template.dart';
|
|
import 'package:todo/ui/confirm.dart';
|
|
|
|
/// The per-note bottom sheet: status/priority chips, and delete.
|
|
class NoteActionsSheet extends StatefulWidget {
|
|
/// Creates the per-note actions sheet for [note].
|
|
const NoteActionsSheet({
|
|
required this.note,
|
|
required this.onChanged,
|
|
required this.onDelete,
|
|
super.key,
|
|
});
|
|
|
|
/// The note being acted on.
|
|
final Note note;
|
|
|
|
/// Called with the edited note whenever a chip changes it.
|
|
final Future<void> Function(Note) onChanged;
|
|
|
|
/// Called once the user confirms deletion.
|
|
final Future<void> Function() onDelete;
|
|
|
|
@override
|
|
State<NoteActionsSheet> createState() => _NoteActionsSheetState();
|
|
}
|
|
|
|
class _NoteActionsSheetState extends State<NoteActionsSheet> {
|
|
late Priority _priority = widget.note.priority;
|
|
late Status _status = widget.note.status;
|
|
|
|
Future<void> _persist() async {
|
|
await widget.onChanged(
|
|
widget.note.copyWith(
|
|
priority: _priority,
|
|
status: _status,
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final firstLine = noteTitle(widget.note.text);
|
|
return SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
firstLine.isEmpty ? '(empty)' : firstLine,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_EnumChips<Status>(
|
|
label: 'Status',
|
|
values: Status.values,
|
|
selected: {_status},
|
|
labelOf: (s) => s.label,
|
|
onSelected: (s) {
|
|
setState(() => _status = s);
|
|
unawaited(_persist());
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
_EnumChips<Priority>(
|
|
label: 'Priority',
|
|
values: Priority.values,
|
|
selected: {_priority},
|
|
labelOf: (p) => p.label,
|
|
onSelected: (p) {
|
|
setState(() => _priority = p);
|
|
unawaited(_persist());
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
OutlinedButton.icon(
|
|
icon: const Icon(Icons.delete_outline),
|
|
label: const Text('Delete note'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Theme.of(context).colorScheme.error,
|
|
),
|
|
onPressed: () async {
|
|
final confirmed = await confirmDestructive(
|
|
context,
|
|
title: 'Delete note?',
|
|
message: 'This cannot be undone.',
|
|
);
|
|
if (!confirmed) return;
|
|
await widget.onDelete();
|
|
if (context.mounted) Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Single-select chip row for an enum (used for per-note priority/status).
|
|
class _EnumChips<T> extends StatelessWidget {
|
|
const _EnumChips({
|
|
required this.label,
|
|
required this.values,
|
|
required this.selected,
|
|
required this.labelOf,
|
|
required this.onSelected,
|
|
});
|
|
|
|
final String label;
|
|
final List<T> values;
|
|
final Set<T> selected;
|
|
final String Function(T) labelOf;
|
|
final ValueChanged<T> onSelected;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: Theme.of(context).textTheme.labelLarge),
|
|
const SizedBox(height: 6),
|
|
Wrap(
|
|
spacing: 8,
|
|
children: [
|
|
for (final v in values)
|
|
ChoiceChip(
|
|
label: Text(labelOf(v)),
|
|
selected: selected.contains(v),
|
|
onSelected: (_) => onSelected(v),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|