mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-11 13:47:45 +01:00
Editing an existing note looked nothing like capturing a new one: it always showed the template dropdown, the View/Guided/Raw pills and the priority/status row. The cause was one missing argument -- both screens already embedded the same NoteEditor, but NoteDetailScreen never passed advancedMode, which defaults to true in the widget while the app-wide AppSettings.advancedMode defaults to false. Extract the shared form as three single-purpose components rather than one widget owning several jobs: - note_meta_row.dart renders the priority/status row (was capture_meta_row.dart; drops the private _MetaDropdown duplicate the detail screen carried, which used DropdownButtonFormField where the shared one uses DropdownButton). - note_form_chrome_gate.dart owns chrome visibility and nothing else, replacing the _chromeVisible flag both screens copy-pasted. - note_form.dart composes those two with NoteEditor. appSettings is now a required parameter through NotesListScreen and NoteDetailScreen. Required, not defaulted: a silent default is exactly what caused this, so it must not be reintroducible. Also in the capture app bar: drop the decorative Icons.list_alt_outlined beside the note count, since the adjacent Icons.list button already names what is counted, and move the count to the right of that button. The finalize-and-clear action becomes Icons.check instead of Icons.add -- it never saved anything (every keystroke is already persisted), so a plus sign misdescribed it. 314 tests, 100% line coverage. Verified in a running web build that edit and capture render identically with advanced mode both off and on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
3.2 KiB
Dart
99 lines
3.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:todo/data/app_settings.dart';
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_repository.dart';
|
|
import 'package:todo/data/note_template.dart';
|
|
import 'package:todo/ui/confirm.dart';
|
|
import 'package:todo/ui/note_form.dart';
|
|
|
|
/// Full-screen view of a single note: read it in full, edit its body through
|
|
/// the shared [NoteForm], change its priority/status, or delete it.
|
|
///
|
|
/// Edits persist immediately (matching the capture screen's autosave), so
|
|
/// there is no explicit save button. The template is detected from the note's
|
|
/// text, falling back to a raw editor for freeform/legacy notes.
|
|
class NoteDetailScreen extends StatefulWidget {
|
|
/// Creates a [NoteDetailScreen] editing [note].
|
|
const NoteDetailScreen({
|
|
required this.note,
|
|
required this.repository,
|
|
required this.appSettings,
|
|
super.key,
|
|
});
|
|
|
|
/// The note being viewed/edited.
|
|
final Note note;
|
|
|
|
/// The store edits persist to.
|
|
final NoteRepository repository;
|
|
|
|
/// Drives whether the metadata row and editor chrome are shown. Required,
|
|
/// not defaulted: this screen silently defaulting it to "advanced" is
|
|
/// exactly what made editing look different from capture.
|
|
final ValueNotifier<AppSettings> appSettings;
|
|
|
|
@override
|
|
State<NoteDetailScreen> createState() => _NoteDetailScreenState();
|
|
}
|
|
|
|
class _NoteDetailScreenState extends State<NoteDetailScreen> {
|
|
late Note _note = widget.note;
|
|
|
|
Future<void> _persist(Note next) async {
|
|
setState(() => _note = next);
|
|
await widget.repository.upsert(next);
|
|
}
|
|
|
|
Future<void> _onTextChanged(String text) =>
|
|
_persist(_note.copyWith(text: text, updatedAt: DateTime.now()));
|
|
|
|
Future<void> _delete() async {
|
|
final confirmed = await confirmDestructive(
|
|
context,
|
|
title: 'Delete note?',
|
|
message: 'This cannot be undone.',
|
|
);
|
|
if (!confirmed) return;
|
|
await widget.repository.delete(_note.id);
|
|
if (mounted) Navigator.of(context).pop();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final title = noteTitle(_note.text);
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(title.isEmpty ? '(empty)' : title),
|
|
actions: [
|
|
IconButton(
|
|
tooltip: 'Delete note',
|
|
icon: const Icon(Icons.delete_outline),
|
|
onPressed: _delete,
|
|
),
|
|
],
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
// The same form capture renders, gated on the same setting, so an
|
|
// edited note and a fresh one cannot look different.
|
|
child: ValueListenableBuilder<AppSettings>(
|
|
valueListenable: widget.appSettings,
|
|
builder: (context, settings, _) => NoteForm(
|
|
advancedMode: settings.advancedMode,
|
|
initialText: _note.text,
|
|
priority: _note.priority,
|
|
status: _note.status,
|
|
onPriorityChanged: (p) => _persist(
|
|
_note.copyWith(priority: p, updatedAt: DateTime.now()),
|
|
),
|
|
onStatusChanged: (s) =>
|
|
_persist(_note.copyWith(status: s, updatedAt: DateTime.now())),
|
|
onChanged: _onTextChanged,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|