todo-app/lib/ui/capture_screen_widget.dart
Krzysztof kuhy Rudnicki 9f2ffdb8a9
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Attach any file to a note, of any size, not only images
📎 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>
2026-10-01 12:17:06 +02:00

69 lines
2.8 KiB
Dart

/// The [CaptureScreen] widget's public surface: its parameters and their
/// contracts. The state that drives them lives in `capture_screen.dart`.
///
/// A `part` rather than a separate library because `createState` returns a
/// private class, so the widget and its State cannot be split across
/// libraries.
part of 'capture_screen.dart';
/// The landing screen: an always-focused text box for jotting an idea.
///
/// Per the product goal "no interruptions, immediate", text is persisted
/// to local storage on *every* keystroke. A note row is created lazily on
/// the first character typed, then updated in place. The "new note" action
/// finalises the current idea and clears the field for the next one.
class CaptureScreen extends StatefulWidget {
/// Creates a [CaptureScreen] backed by [repository].
const CaptureScreen({
required this.repository,
required this.appSettings,
this.analytics,
this.httpClient,
this.localBackup,
this.firebaseFactory,
this.stateStore,
this.attachmentPicking,
super.key,
});
/// The store the captured note is persisted to on every keystroke.
final NoteRepository repository;
/// App-wide preferences, currently just `advancedMode`: whether the
/// priority/status row and the routine save/sync status line are shown.
/// A sync failure is always surfaced regardless of this flag.
final ValueNotifier<AppSettings> appSettings;
/// Interaction-only usage analytics (taps, navigation, sync outcomes —
/// never note text). Null in tests that don't exercise it, so the prefs
/// round-trip `logEvent` does on every call never has to be awaited by a
/// test that didn't ask for it.
final AnalyticsService? analytics;
/// Builds the Firebase backend. Injected so tests can supply a fake, or
/// null to assert the pre-migration GitHub-only path still works.
final Future<FirebaseRestClient?> Function()? firebaseFactory;
/// Revision cache. Injected so tests need no application-support directory.
final SyncStateStore? stateStore;
/// Injectable HTTP client for the sync path. Production leaves this null
/// (the GitHubClient creates its own); tests pass a mock so the configured
/// sync flow can be exercised without real network access.
final http.Client? httpClient;
/// Injectable local-disk backup. Production leaves this null (a platform
/// file-backed instance is created); tests pass a fake with in-memory IO.
final LocalBackup? localBackup;
/// The 📎 picker; null uses the platform one. Injected by tests.
final AttachmentPicking? attachmentPicking;
/// How long focus must stay lost before a background sync fires. Public so
/// tests can pump exactly past it.
static const autoSyncDebounce = Duration(seconds: 5);
@override
State<CaptureScreen> createState() => _CaptureScreenState();
}