Notechondria

Version: 0.1.37 Build Date: 2026-04-18T15:00

What's Changed

Editor \u2014 attachment upload works offline

Fixed the note-editor attachment button that did not trigger an upload on local drafts (negative IDs) or in offline mode.

Before: _pickAndUploadAttachment short-circuited with a "Save the note before adding attachments." SnackBar whenever noteId < 0 or the upload callback was null, and the host-side _uploadNoteAttachment threw "Sign in to upload attachments." whenever the token was empty. On Safari running a local-only demo, the user saw nothing happen.

After:

  • On a saved cloud note with a session, the old cloud-upload path runs; on failure it now falls through to the offline-queue path instead of just toasting an error.
  • On a local draft or when the host rejects mid-upload, the editor embeds a data:<content-type>;base64,... URI directly into the note markdown so the attachment renders inline in preview right away, and pushes a {filename, content_type, bytes_base64, queued_at} record onto metadata_json['queued_attachments'] so the draft carries the binary with it until it syncs.
  • A new host helper _promoteQueuedAttachments(note, metadata) runs at the end of both _syncLocalDraft success paths (cloud-copy update and fresh-create). It iterates the queued list, uploads each payload against the now-cloud-id note via widget.client.uploadNoteAttachment, rewrites the inline data URI in the content to the server's returned URL, patches the note via updateNote, and drops the queue so it doesn't retry forever. Any single-attachment failure logs at warning level (Editor.Sync.Notes/attachment.promote) and the remaining queued entries are still promoted.
  • Every message emitted along the new path follows AGENTS.md \u00a71.7 shape (Editor.UI/editor.attachment, Editor.Sync.Notes/attachment.queue, Editor.Sync.Notes/attachment.promote, Editor.Sync.Notes/attachment.upload).
  • Small helper _guessContentType(filename, bytes) picks a reasonable image/... or application/octet-stream for the data URI so markdown preview renders images inline without a round-trip.

Portal and planner were not touched this round \u2014 their note editors are inlined into lib/modules/learner.dart and wire the upload callback differently; a follow-up round will replicate the offline-queue pattern there.

Note editor UX polish

  • Last-saved subtitle floats at lower-left. The _SaveStatus(lastSavedAt, errorMessage, saving) widget was previously rendered inline with the editor-mode dropdown in the top bar. It now lives as a Positioned(left: 8, bottom: 8, \u2026) overlay inside the editor Stack and inherits a small, dimmed DefaultTextStyle so the subtitle hovers at the lower-left of the window without grabbing pointer events (IgnorePointer wrap). The top-bar layout reclaims the space for the title field on wide layouts.
  • Plain-text editor borderless. The multi-line TextField under the "P" editor mode used OutlineInputBorder(); replaced with InputBorder.none in editor / planner / portal so the input reads as an open canvas. The markdown live editor path was already borderless.
  • Removed "Stored locally until you sync" hint. The small bottom-right caption on local-draft note preview cards (editor/planner/portal/lib/modules/learner.dart) is gone. Planner and portal still show the non-draft "Course metadata stays editable from the editor details panel" caption; only the local-draft branch of that ternary is removed.

Docs

  • docs/TODO.md expanded with detailed specs for three deferred tasks that did not fit in this round:
    • "Editor mode selection \u2192 '...' menu" (UX move, bundled with an extra "Edit note meta" entry).
    • "Offline mode toggle" in shared AppPreferencesCard gating auto-sync + lazy public-notes load.
    • "Download local user data" / "Restore from local imports" replacing the minimal config-file download with a versioned .nchron zip format carrying every persisted bucket + an attachments/ directory. Spec includes the format, version-gating rules for the importer, cross-app portability policy, and a three-commit split plan.

Files Changed

New

  • docs/versions/0.1.37.md (this file).

Modified

  • VERSION: 0.1.36 \u2192 0.1.37.
  • docs/TODO.md: deferred-task specs added.
  • frontend/editor_app/lib/modules/note_editor.dart: _pickAndUploadAttachment rewritten for offline queue; _guessContentType helper added; plain-text TextField border dropped; _SaveStatus hoisted out of the LayoutBuilder and added to the editor Stack as a lower-left floating subtitle.
  • frontend/editor_app/lib/app_shell.dart: _uploadNoteAttachment missing-session message rewritten; _promoteQueuedAttachments(note, metadata) helper added; both _syncLocalDraft success paths now return _promoteQueuedAttachments(\u2026).
  • frontend/editor_app/lib/modules/learner.dart: "Stored locally until you sync" caption block removed.
  • frontend/planner_app/lib/modules/learner.dart: "Stored locally until you sync" branch of the ternary removed (keeps the non-draft caption); plain-text TextField border dropped.
  • frontend/portal_app/lib/modules/learner.dart: same "Stored locally" ternary branch removed; plain-text TextField border dropped.

Verification

  • editor_app: flutter analyze \u2014 52 issues (unchanged vs 0.1.36; the new code introduces no new lint hits).
  • planner_app: flutter analyze \u2014 70 issues (unchanged).
  • portal_app: flutter analyze \u2014 68 issues (unchanged).
  • All three: flutter test test/smoke_test.dart -r compact \u2014 passes.

Notes / follow-ups

  • Deferred this round (tracked in docs/TODO.md):
    • Move editor-mode selection out of the top bar into the "..." menu (adds "Edit note meta" + "Switch editor" options).
    • Offline-mode toggle in the shared AppPreferencesCard.
    • .nchron versioned data export / import (replaces the current minimal config-file download).
  • Planner and portal still carry their own inlined note editors inside lib/modules/learner.dart. The attachment offline-queue pattern from 0.1.37 should be replicated there when those files are next touched; their upload paths are not broken the same way because neither exposes a client-side attachment button yet (check the widget.onUploadAttachment != null guard on the FAB in each file).