mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 20:27:01 +01:00
Some checks failed
📎 now picks any file (Android: Camera / Gallery / File; desktop: an
unfiltered dialog) and the desktop page accepts any dropped file. Photo and
screenshot formats up to 64 MB are still re-encoded and shown inline;
everything else is stored byte for byte on dufs beside the images and linked
as [name](url), shown as a row with its size that opens in the platform's
app (Android) or a new browser tab (desktop). BACKLOG.md rewrites file links
to ~/data/cloud/todo-images/ like image links.
There is no size cap, so no layer holds a file in memory: uploads and
downloads stream through DufsImageClient, writes land in a .tmp renamed when
whole (an interrupted download no longer leaves a truncated file that looks
done), the desktop page POSTs the browser's own Blob with fetch, and Android's
"File" uses file_picker, because file_selector's Android side reads the whole
file into memory on the UI thread. Verified with a 1 GB file end to end on
desktop (peak RSS +36 MB) and on an API 36 emulator.
The wrapper serves attachments from the origin that holds the GitHub token
and the notes, so only script-free types open inline (nosniff), the rest
download; writes carrying another site's Origin are refused (this also
closes cross-site overwrites of the backup files); and attachment routes run
concurrently so a long upload or a playing video cannot stall the page.
Also: cupertino_icons 2.0.0 (unused, but the freshness gate blocks the
pubspec otherwise), open_file 4.0.0 and file_picker 13.1.0 pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
4.8 KiB
Dart
140 lines
4.8 KiB
Dart
/// Composes the note-editing form shared by capture and the detail screen.
|
|
///
|
|
/// Composition only, no logic of its own: [NoteFormChromeGate] decides
|
|
/// visibility, [NoteMetaRow] renders the priority/status row, and [NoteEditor]
|
|
/// owns the text. Both surfaces render this so an edited note and a fresh
|
|
/// capture cannot look different -- the divergence this replaced was the
|
|
/// detail screen defaulting `advancedMode` to true while the app-wide setting
|
|
/// defaults to false.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_template.dart';
|
|
import 'package:todo/ui/image_attach.dart';
|
|
import 'package:todo/ui/images/attachment_strip.dart';
|
|
import 'package:todo/ui/note_editor.dart';
|
|
import 'package:todo/ui/note_form_chrome_gate.dart';
|
|
import 'package:todo/ui/note_meta_row.dart';
|
|
|
|
/// The metadata row above a [NoteEditor], gated on advanced mode, and the
|
|
/// attached-image thumbnails below it.
|
|
class NoteForm extends StatefulWidget {
|
|
/// Creates the form for the given note values.
|
|
const NoteForm({
|
|
required this.advancedMode,
|
|
required this.priority,
|
|
required this.status,
|
|
required this.onPriorityChanged,
|
|
required this.onStatusChanged,
|
|
required this.onChanged,
|
|
this.initialText = '',
|
|
this.initialTemplate,
|
|
this.autofocus = false,
|
|
this.attachController,
|
|
super.key,
|
|
});
|
|
|
|
/// Whether the metadata row and the editor's own chrome may be shown.
|
|
final bool advancedMode;
|
|
|
|
/// The note's current priority.
|
|
final Priority priority;
|
|
|
|
/// The note's current status.
|
|
final Status status;
|
|
|
|
/// Called with a newly picked priority.
|
|
final ValueChanged<Priority> onPriorityChanged;
|
|
|
|
/// Called with a newly picked status.
|
|
final ValueChanged<Status> onStatusChanged;
|
|
|
|
/// Called with the freshly assembled note text on every edit.
|
|
final ValueChanged<String> onChanged;
|
|
|
|
/// Existing note text to load. Empty for a fresh draft.
|
|
final String initialText;
|
|
|
|
/// Template to author with. Null detects it from [initialText], which is
|
|
/// what opening an existing note wants.
|
|
final NoteTemplate? initialTemplate;
|
|
|
|
/// Autofocus the editor, so a fresh capture needs zero taps before typing.
|
|
final bool autofocus;
|
|
|
|
/// Where the screen's attach actions insert image links.
|
|
final ImageAttachController? attachController;
|
|
|
|
@override
|
|
State<NoteForm> createState() => _NoteFormState();
|
|
}
|
|
|
|
class _NoteFormState extends State<NoteForm> {
|
|
// The live text, for the thumbnail strip only: a notifier so a keystroke
|
|
// rebuilds the strip, not the editor.
|
|
late final ValueNotifier<String> _text = ValueNotifier(widget.initialText);
|
|
bool _previewing = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_text.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onChanged(String text) {
|
|
_text.value = text;
|
|
widget.onChanged(text);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return NoteFormChromeGate(
|
|
advancedMode: widget.advancedMode,
|
|
builder:
|
|
(context, {required chromeVisible, required onChromeVisibleChanged}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (chromeVisible) ...[
|
|
NoteMetaRow(
|
|
priority: widget.priority,
|
|
status: widget.status,
|
|
onPriorityChanged: widget.onPriorityChanged,
|
|
onStatusChanged: widget.onStatusChanged,
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
Expanded(
|
|
child: NoteEditor(
|
|
initialText: widget.initialText,
|
|
initialTemplate: widget.initialTemplate,
|
|
initialMode: NoteEditorMode.raw,
|
|
advancedMode: widget.advancedMode,
|
|
priority: widget.priority,
|
|
onPriorityChanged: widget.onPriorityChanged,
|
|
onChromeVisibleChanged: onChromeVisibleChanged,
|
|
autofocus: widget.autofocus,
|
|
onChanged: _onChanged,
|
|
attachController: widget.attachController,
|
|
onModeChanged: (mode) => setState(
|
|
() => _previewing = mode == NoteEditorMode.preview,
|
|
),
|
|
),
|
|
),
|
|
// A sibling of the editor's Expanded, not nested inside it:
|
|
// an extra Column around the expanding field breaks its
|
|
// height in release builds (see NoteEditorRawField).
|
|
if (!_previewing)
|
|
ValueListenableBuilder<String>(
|
|
valueListenable: _text,
|
|
builder: (context, text, _) => AttachmentStrip(text: text),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|