mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-07-04 12:03:10 +02:00
Fix guided mode: full-screen TextField and hide Save FAB
Rework guided-mode layout so the text field fills the full available screen height. The previous approach nested _buildStepPage (a Column with its own Expanded) inside NoteEditor's outer Expanded — a nested-Column/Expanded chain that collapses silently in release builds. Fix: return _buildStepPage directly from build() when in guided mode, making the TextField a first-level Expanded child of the same Column as _buildRaw uses. This is the same depth at which expands:true works. Also hide the Save FAB (_chromeVisible gate in CaptureScreen) while guided mode is active so it cannot overlap the Next/Done button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Cb7oE5Xsc8zSBHHtT6qwa
This commit is contained in:
parent
5ca289ca81
commit
6398ce9275
@ -420,11 +420,13 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: _chromeVisible
|
||||||
onPressed: _saveAndReset,
|
? FloatingActionButton.extended(
|
||||||
icon: const Icon(Icons.check),
|
onPressed: _saveAndReset,
|
||||||
label: const Text('Save'),
|
icon: const Icon(Icons.check),
|
||||||
),
|
label: const Text('Save'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -338,72 +338,63 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
if (_enteringGuided) return _buildGuidedEntryWizard(theme);
|
if (_enteringGuided) return _buildGuidedEntryWizard(theme);
|
||||||
|
|
||||||
// Guided (the bare stepper) hides the template/mode chrome entirely —
|
// Guided hides the template/mode chrome entirely. Return the flat step
|
||||||
// that's the point of "Guided": just the stepper, no top-bar noise. A
|
// page directly so the TextField sits in THIS column's Expanded — same
|
||||||
// single back arrow is the only way out, restoring the full chrome.
|
// depth as _buildRaw, avoiding the nested-Column/Expanded layout issue
|
||||||
final bareGuided = _mode == NoteEditorMode.guided;
|
// that silently collapses the inner flex space in release builds.
|
||||||
|
if (_mode == NoteEditorMode.guided) return _buildStepPage(theme);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (bareGuided)
|
DropdownButtonFormField<String>(
|
||||||
Align(
|
initialValue: _template.id,
|
||||||
alignment: Alignment.centerLeft,
|
isDense: true,
|
||||||
child: IconButton(
|
decoration: const InputDecoration(
|
||||||
icon: const Icon(Icons.arrow_back),
|
labelText: 'Template',
|
||||||
tooltip: 'Exit guided',
|
|
||||||
onPressed: _exitGuided,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
DropdownButtonFormField<String>(
|
|
||||||
initialValue: _template.id,
|
|
||||||
isDense: true,
|
isDense: true,
|
||||||
decoration: const InputDecoration(
|
border: OutlineInputBorder(),
|
||||||
labelText: 'Template',
|
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
isDense: true,
|
),
|
||||||
border: OutlineInputBorder(),
|
items: [
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
for (final t in NoteTemplate.all)
|
||||||
),
|
DropdownMenuItem(value: t.id, child: Text(t.label)),
|
||||||
items: [
|
],
|
||||||
for (final t in NoteTemplate.all)
|
onChanged: (id) {
|
||||||
DropdownMenuItem(value: t.id, child: Text(t.label)),
|
if (id == null) return;
|
||||||
|
_switchTemplate(NoteTemplate.all.firstWhere((t) => t.id == id));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SegmentedButton<NoteEditorMode>(
|
||||||
|
showSelectedIcon: false,
|
||||||
|
segments: [
|
||||||
|
const ButtonSegment(
|
||||||
|
value: NoteEditorMode.preview,
|
||||||
|
icon: Icon(Icons.visibility_outlined),
|
||||||
|
label: Text('View'),
|
||||||
|
),
|
||||||
|
// Guided is offered for any structured template; tapping it
|
||||||
|
// on an empty draft opens the wizard (see _setMode), and is
|
||||||
|
// blocked at switch time if the raw text no longer conforms.
|
||||||
|
if (!_template.isFreeform)
|
||||||
|
const ButtonSegment(
|
||||||
|
value: NoteEditorMode.guided,
|
||||||
|
icon: Icon(Icons.checklist),
|
||||||
|
label: Text('Guided'),
|
||||||
|
),
|
||||||
|
const ButtonSegment(
|
||||||
|
value: NoteEditorMode.raw,
|
||||||
|
icon: Icon(Icons.notes),
|
||||||
|
label: Text('Raw'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onChanged: (id) {
|
selected: {_mode},
|
||||||
if (id == null) return;
|
onSelectionChanged: (s) => _setMode(s.first),
|
||||||
_switchTemplate(NoteTemplate.all.firstWhere((t) => t.id == id));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: SegmentedButton<NoteEditorMode>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
segments: [
|
|
||||||
const ButtonSegment(
|
|
||||||
value: NoteEditorMode.preview,
|
|
||||||
icon: Icon(Icons.visibility_outlined),
|
|
||||||
label: Text('View'),
|
|
||||||
),
|
|
||||||
// Guided is offered for any structured template; tapping it
|
|
||||||
// on an empty draft opens the wizard (see _setMode), and is
|
|
||||||
// blocked at switch time if the raw text no longer conforms.
|
|
||||||
if (!_template.isFreeform)
|
|
||||||
const ButtonSegment(
|
|
||||||
value: NoteEditorMode.guided,
|
|
||||||
icon: Icon(Icons.checklist),
|
|
||||||
label: Text('Guided'),
|
|
||||||
),
|
|
||||||
const ButtonSegment(
|
|
||||||
value: NoteEditorMode.raw,
|
|
||||||
icon: Icon(Icons.notes),
|
|
||||||
label: Text('Raw'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: {_mode},
|
|
||||||
onSelectionChanged: (s) => _setMode(s.first),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Expanded(child: _buildBody(theme)),
|
Expanded(child: _buildBody(theme)),
|
||||||
],
|
],
|
||||||
@ -520,7 +511,8 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
case NoteEditorMode.raw:
|
case NoteEditorMode.raw:
|
||||||
return _buildRaw(theme);
|
return _buildRaw(theme);
|
||||||
case NoteEditorMode.guided:
|
case NoteEditorMode.guided:
|
||||||
return _buildStepPage(theme);
|
// build() short-circuits to _buildStepPage before reaching here.
|
||||||
|
return _buildRaw(theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,6 +533,10 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full-screen per-step view returned directly from [build] so the
|
||||||
|
/// [TextField] is a first-level [Expanded] child of THIS column — the same
|
||||||
|
/// depth as [_buildRaw]. Nesting it inside a second Column broke the
|
||||||
|
/// flutter constraint chain in release builds (inner Expanded got 0 height).
|
||||||
Widget _buildStepPage(ThemeData theme) {
|
Widget _buildStepPage(ThemeData theme) {
|
||||||
_ensureControllers(_template);
|
_ensureControllers(_template);
|
||||||
final sections = _template.sections;
|
final sections = _template.sections;
|
||||||
@ -552,9 +548,18 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Progress bar + step counter
|
// Back arrow — only exit from guided mode.
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
tooltip: 'Exit guided',
|
||||||
|
onPressed: _exitGuided,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Progress bar + step counter.
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text('${idx + 1} / $total', style: theme.textTheme.labelMedium),
|
Text('${idx + 1} / $total', style: theme.textTheme.labelMedium),
|
||||||
@ -565,47 +570,52 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Section label + helper + text field
|
// 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 _buildRaw, so expands:true works correctly.
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||||
child: Column(
|
child: TextField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
controller: controller,
|
||||||
children: [
|
autofocus: widget.autofocus && idx == 0,
|
||||||
const SizedBox(height: 8),
|
expands: true,
|
||||||
Text(
|
maxLines: null,
|
||||||
section.isTitle ? 'title' : section.label,
|
keyboardType: TextInputType.multiline,
|
||||||
style: theme.textTheme.titleMedium,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
const SizedBox(height: 4),
|
hintText: section.hint,
|
||||||
Text(
|
border: const OutlineInputBorder(),
|
||||||
section.helper,
|
isDense: true,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
),
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
onChanged: (_) {
|
||||||
),
|
setState(() {});
|
||||||
),
|
_emit();
|
||||||
const SizedBox(height: 12),
|
},
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
autofocus: widget.autofocus && idx == 0,
|
|
||||||
maxLines: section.inline ? 1 : null,
|
|
||||||
minLines: section.inline ? 1 : 6,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: section.hint,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
onChanged: (_) {
|
|
||||||
setState(() {});
|
|
||||||
_emit();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Navigation buttons — below Expanded so they stay above the keyboard
|
// Navigation buttons — sibling of Expanded so keyboard pushes them up.
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user