mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 20:27: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>
152 lines
5.0 KiB
Dart
152 lines
5.0 KiB
Dart
/// The guided stepper's full-screen per-step view.
|
|
///
|
|
/// Split out of `note_editor.dart` for file size. Stateless on purpose: the
|
|
/// controllers, the current step and the template all stay owned by
|
|
/// `_NoteEditorState`, and are passed in — this widget only lays them out.
|
|
///
|
|
/// Returned directly from the editor's `build` so the [TextField] is a
|
|
/// first-level [Expanded] child of THIS column, the same depth as the raw
|
|
/// editor's. Nesting it inside a second Column broke the flutter constraint
|
|
/// chain in release builds (the inner Expanded got 0 height), so the flat
|
|
/// structure here is load-bearing rather than incidental.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:todo/data/note_template.dart';
|
|
|
|
/// One step of the guided editor: label, guidance, input, and navigation.
|
|
class NoteEditorStepPage extends StatelessWidget {
|
|
/// Creates the step view for [section], step [index] of [total].
|
|
const NoteEditorStepPage({
|
|
required this.section,
|
|
required this.controller,
|
|
required this.index,
|
|
required this.total,
|
|
required this.autofocus,
|
|
required this.onChanged,
|
|
required this.onGoToStep,
|
|
required this.onExit,
|
|
super.key,
|
|
});
|
|
|
|
/// The template section this step edits.
|
|
final TemplateSection section;
|
|
|
|
/// Controller for this section's text, owned by the editor.
|
|
final TextEditingController controller;
|
|
|
|
/// Zero-based index of this step, already clamped to the section range.
|
|
final int index;
|
|
|
|
/// How many steps the template has in total.
|
|
final int total;
|
|
|
|
/// Whether the input should take focus on first build.
|
|
final bool autofocus;
|
|
|
|
/// Called on every keystroke so the editor can re-emit the assembled text.
|
|
final VoidCallback onChanged;
|
|
|
|
/// Called with the step index to move to.
|
|
final ValueChanged<int> onGoToStep;
|
|
|
|
/// Called by the back arrow and by "Done" on the last step.
|
|
final VoidCallback onExit;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Back arrow — only exit from guided mode.
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
tooltip: 'Exit guided',
|
|
onPressed: onExit,
|
|
),
|
|
),
|
|
// Progress bar + step counter.
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 4),
|
|
child: Row(
|
|
children: [
|
|
Text('${index + 1} / $total', style: theme.textTheme.labelMedium),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: LinearProgressIndicator(value: (index + 1) / total),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Section label + helper text.
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
section.isTitle ? 'title' : section.label,
|
|
style: theme.textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
section.helper,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
),
|
|
// TextField fills all remaining space — directly in this Column's
|
|
// Expanded, same as the raw editor, so expands:true works correctly.
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
|
child: TextField(
|
|
controller: controller,
|
|
autofocus: autofocus && index == 0,
|
|
expands: true,
|
|
maxLines: null,
|
|
keyboardType: TextInputType.multiline,
|
|
textAlignVertical: TextAlignVertical.top,
|
|
decoration: InputDecoration(
|
|
hintText: section.hint,
|
|
border: const OutlineInputBorder(),
|
|
isDense: true,
|
|
),
|
|
onChanged: (_) => onChanged(),
|
|
),
|
|
),
|
|
),
|
|
// Navigation buttons — sibling of Expanded so keyboard pushes them up.
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
if (index > 0)
|
|
TextButton(
|
|
onPressed: () => onGoToStep(index - 1),
|
|
child: const Text('Back'),
|
|
),
|
|
const Spacer(),
|
|
if (index < total - 1)
|
|
FilledButton(
|
|
onPressed: () => onGoToStep(index + 1),
|
|
child: const Text('Next'),
|
|
)
|
|
else
|
|
FilledButton(onPressed: onExit, child: const Text('Done')),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|