mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 22:47:01 +01:00
702 -> 220 lines, decomposed rather than just moved, because a single
~600-line State class was the actual problem:
note_editor_document.dart the text model -- template, controllers, and
which of the two sources (raw body vs. section
fields) is authoritative. None of it needed a
BuildContext or a setState, which is what made
it the honest seam. The raw<->guided conversions
and the initial-template detection moved here as
tryAdoptGuided/materialiseToRaw/forInitialText.
note_editor_wizard.dart the two-step priority+template wizard, which now
owns its own step state instead of parking three
fields on the editor.
note_editor_step_page.dart the guided per-step view.
note_editor_chrome.dart the template picker + mode toggle.
note_editor_raw_field.dart the raw-mode text field.
note_editor_mode.dart the mode enum, re-exported so callers are
unaffected.
note_editor_{widget,views} parts, not libraries: createState returns a
private class, so the widget and its State
cannot be split across libraries.
The layout comments about Expanded/expands:true are load-bearing (nesting
broke the constraint chain in release builds before), so they travelled with
the code they describe rather than being summarised away.
Verified: 313 tests pass, flutter analyze clean. Covered lines went UP,
1922 -> 1958, because the wizard's state is now exercised in a widget of its
own; every new file reports 100%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
4.2 KiB
Dart
139 lines
4.2 KiB
Dart
/// The two-step wizard shown before the guided stepper on an empty draft.
|
|
///
|
|
/// Split out of `note_editor.dart` for file size. It owns its own step index
|
|
/// and pending choices — the editor only cares about the two values that come
|
|
/// back out, so keeping that state here rather than in `_NoteEditorState` is
|
|
/// both smaller and better scoped.
|
|
///
|
|
/// Priority and template are asked once, up front, because those choices only
|
|
/// make sense before there is anything to guide.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_template.dart';
|
|
|
|
/// Asks for a priority, then a template, then hands both back via [onStart].
|
|
class NoteEditorWizard extends StatefulWidget {
|
|
/// Creates the wizard seeded with [initialPriority] and [initialTemplate].
|
|
const NoteEditorWizard({
|
|
required this.initialPriority,
|
|
required this.initialTemplate,
|
|
required this.onStart,
|
|
required this.onCancel,
|
|
super.key,
|
|
});
|
|
|
|
/// Priority the first step opens on.
|
|
final Priority initialPriority;
|
|
|
|
/// Template the second step opens on.
|
|
final NoteTemplate initialTemplate;
|
|
|
|
/// Called with both choices when the user presses "Start".
|
|
final void Function(Priority priority, NoteTemplate template) onStart;
|
|
|
|
/// Called when the user dismisses the wizard.
|
|
final VoidCallback onCancel;
|
|
|
|
@override
|
|
State<NoteEditorWizard> createState() => _NoteEditorWizardState();
|
|
}
|
|
|
|
class _NoteEditorWizardState extends State<NoteEditorWizard> {
|
|
late int _step = 0;
|
|
late Priority _priority = widget.initialPriority;
|
|
late NoteTemplate _template = widget.initialTemplate;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
tooltip: 'Cancel',
|
|
onPressed: widget.onCancel,
|
|
),
|
|
Text('Step ${_step + 1} of 2', style: theme.textTheme.labelLarge),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
if (_step == 0) ..._priorityStep() else ..._templateStep(),
|
|
],
|
|
);
|
|
}
|
|
|
|
List<Widget> _priorityStep() {
|
|
return [
|
|
DropdownButtonFormField<Priority>(
|
|
initialValue: _priority,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Priority',
|
|
isDense: true,
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
items: [
|
|
for (final p in Priority.values)
|
|
DropdownMenuItem(value: p, child: Text(p.label)),
|
|
],
|
|
onChanged: (p) {
|
|
if (p != null) setState(() => _priority = p);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: FilledButton(
|
|
onPressed: () => setState(() => _step = 1),
|
|
child: const Text('Next'),
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _templateStep() {
|
|
final templates = NoteTemplate.all.where((t) => !t.isFreeform).toList();
|
|
return [
|
|
DropdownButtonFormField<String>(
|
|
initialValue: _template.id,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Template',
|
|
isDense: true,
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
items: [
|
|
for (final t in templates)
|
|
DropdownMenuItem(value: t.id, child: Text(t.label)),
|
|
],
|
|
onChanged: (id) {
|
|
if (id == null) return;
|
|
setState(() => _template = templates.firstWhere((t) => t.id == id));
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => setState(() => _step = 0),
|
|
child: const Text('Back'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
FilledButton(
|
|
onPressed: () => widget.onStart(_priority, _template),
|
|
child: const Text('Start'),
|
|
),
|
|
],
|
|
),
|
|
];
|
|
}
|
|
}
|