Notechondria

Version: 0.1.42 Build Date: 2026-04-18T12:00

What's Changed

Attachment CDN rework, commit 3 of 3: preview parity + attachments list

Wraps up the three-commit plan opened in 0.1.40 / continued in 0.1.41. Planner and portal now render local:// URLs the same way the editor does (so drafts imported from an editor-exported .nchron archive preview correctly), and the editor grows a compact "Attachments" list bottom-sheet — the sub-surface called out in the 0.1.41 follow-ups.

Planner + portal preview parity

  • frontend/planner_app/lib/core/helpers.dart and frontend/portal_app/lib/core/helpers.dart: new _localAttachmentImageBuilder(MarkdownImageConfig) helper, identical to the editor's. Fetches bytes from LocalAttachmentStore for local:// URIs and renders via Image.memory; falls through to Image.network for http(s)://. Missing / evicted entries render a small warning pill naming the attachment.
  • Both learner.dart MarkdownBody(...) call sites (planner_app/lib/modules/learner.dart, portal_app/lib/modules/learner.dart) now pass sizedImageBuilder: _localAttachmentImageBuilder.
  • Planner and portal do not get the picker / queue path this round — neither app has an attachment picker today, so wiring the preview side is enough to correctly render drafts synced from the editor. If the portal/planner grow their own picker, they can reuse the editor's _pickAndUploadAttachment pattern verbatim.

Attachments list sub-surface (editor only)

  • Toolbar FAB region in note_editor.dart changed from a single attach FAB to a Column of two FloatingActionButton.small widgets with distinct heroTags (editor-attachments-list + editor-attach-file) to avoid Hero animation conflicts.
  • New _openAttachmentsList() method opens a showModalBottomSheet capped at 70% of screen height showing:
    • A title ("Attachments") and a subtitle with the local / uploaded counts.
    • One row per entry in metadata_json['queued_attachments'] (local attachments, with delete action) + one row per http(s):// URL extracted from the note body via the r'(?:!\[[^\]]*\]|\[[^\]]*\])\((https?://[^)\s]+)\)' regex (uploaded attachments, with copy-link action).
  • New helpers: _readQueuedAttachmentEntries, _extractCloudAttachmentUrls, _filenameFromUrl, and _deleteLocalAttachment (calls LocalAttachmentStore.delete(localUrl:), strips the queue entry from metadata, removes body lines containing the localUrl, and logs Editor.UI/editor.attachment.delete).
  • New private _AttachmentSheetRow widget at the bottom of the file. For image/* local attachments it renders a 40×40 Image.memory thumbnail via FutureBuilder; otherwise it picks an icon by content-type family. Title = filename, subtitle = formatted size + content-type + local/uploaded tag, trailing action = delete for locals, copy-link for uploaded.

Files Changed

New

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

Modified

Verification

  • editor_app: flutter analyze — 54 issues, same as 0.1.41. No errors. flutter test test/smoke_test.dart — passes.
  • planner_app: flutter analyze — 70 issues, same as 0.1.41. flutter test test/smoke_test.dart — passes.
  • portal_app: flutter analyze — 68 issues, same as 0.1.41. flutter test test/smoke_test.dart — passes.

Notes / follow-ups

The attachment CDN rework is now functionally complete across all three apps. Remaining deferred work carried over from 0.1.41:

IndexedDB web backend (deferred)

The web stub in notechondria_shared/lib/src/local_attachment_store.dart (_WebLocalAttachmentBackend) still stores bytes in an in-memory Map<String, Uint8List> keyed by local:// URL, which means attachments are lost when the tab is closed or refreshed. Next round should swap it for an idb_shim-backed implementation:

  • Open an IndexedDB database notechondria_attachments (version 1) on first open().
  • Object store entries keyed by local://<note_uuid>/<filename> with value {bytes: Uint8List, content_type: String, size_bytes: int, created_at: int}.
  • put / getBytes / delete become transaction('entries', 'readwrite' | 'readonly').objectStore('entries').put / get / delete.
  • totalBytes() iterates the store and sums size_bytes (cached in memory after first walk).
  • migrateBase64Drafts walks existing drafts as today; the only platform-specific piece is the backend.
  • Add idb_shim: ^2.6.0 to notechondria_shared/pubspec.yaml.
  • Tests in notechondria_shared/test/local_attachment_store_test.dart already cover native; add a web test gated by @TestOn('browser') that uses idb_shim/idb_browser.dart.

Storage-budget UI surface (deferred)

LocalAttachmentStore.totalBytes() exists but is not yet read by any UI. Next round should surface it in the debug log window (app_shell.dart's shared debug card) and in settings:

  • Debug log card: a small footer row "Local attachments: ${formatBytes(total)} across ${entryCount} files" refreshed alongside the existing health chips.
  • Settings: a new "Storage" section under the existing "Data" panel showing local attachment total + a "Clear unused local attachments" button that walks metadata_json['queued_attachments'] across all drafts and deletes any orphaned store entries.
  • Display threshold warning pill when totalBytes() > 500 MB to nudge the user toward a sync.