Notechondria
Version: 0.1.80 Build Date: 2026-04-27T00:30
What's Changed
AppShellDraftHelpersMixin — sixth shared mixin shipped
Continues the cross-app deduplication started in 0.1.54 (Log),
0.1.55 (AuthActions), 0.1.56 (OAuth), 0.1.78 (LocalPersist), and
0.1.79 (CourseHelpers). One more mixin from docs/TODO.md lands;
two remain (Session, HttpClientInternals).
-
New
frontend/notechondria_shared/lib/src/app_shell/app_shell_draft_helpers_mixin.dartexposes two byte-identical offline-draft helpers as concrete mixin methods:storeLocalDraft(draft, {incrementCreated})— movesdraftto the front oflocalDraftsif it's already there (matched byid), otherwise inserts it. ReplaceslocalDraftswith an unmodifiable list view. Optionally bumpslocalStats['local_drafts_created']. Does NOT callsetStateor persist to disk; callers handle both.buildOfflineFallbackDraft({sourceNote, payload})— the fallback path when a cloudPOST /notes/orPATCH /notes/<id>/fails (no token, network error, server 5xx). Looks for an existing local draft pointing atsourceNote.id(viametadata.offline_source_note_id), then constructs a fresh draft via the per-appbuildLocalDrafthook withpayloadoverlayingsourceNotedefaults.
Setters added to the abstract surface
These methods MUTATE in-memory state, so the mixin needs writable access. Four new abstract members on the State class:
set localDrafts(List<Map<String, dynamic>>)— write-back to the private_localDraftsfield. The corresponding getter already exists fromAppShellLocalPersistMixin; both are satisfied by the same field.set localStats(Map<String, dynamic>)— same pattern.Map<String, dynamic> decodeNoteMetadata(String raw)— forwards to each app's top-level_decodeNoteMetadata(incore/helpers.dart; same body in all three apps).Map<String, dynamic> buildLocalDraft({...})— forwards to each app's_buildLocalDraft(incore/local_course_builders.dart). Eight named params; defaults match the per-app implementations exactly so callers can omit optional args without changing behavior.
Why the hook-based design
Both helpers depend on per-app code (_decodeNoteMetadata lives
as a top-level function in app-private code; _buildLocalDraft
is an extension method on _AppShellState). The shared mixin
can't reach that code directly. Two options were considered:
- Lift
decodeNoteMetadataandbuildLocalDraftintonotechondria_sharedtoo. Tempting, but_buildLocalDraftreaches_LocalAppStore.newDraftId()(per-app), and pulling that out has cascading effects. Deferred. - Keep the per-app helpers in place, expose them as abstract hooks. Lower-risk; mechanical port. Chosen for this round.
If a future round consolidates the helpers, the mixin's hook signatures stay stable — only the State's overrides change from "forward to local helper" to "call shared helper".
Three apps ported
Each app's wiring follows the same shape:
-
_AppShellStatemixes inAppShellDraftHelpersMixin<AppShell>(slotted betweenAppShellCourseHelpersMixinandAppShellAuthActionsMixinin thewithclause). -
Two new setters + two new method overrides per app —
set localDrafts,set localStats,decodeNoteMetadata,buildLocalDraft. The first two are one-liners; the last two forward to existing local helpers. -
core/draft_helpers.dartreduced to a shim with a documentation pointer — the file stays so the parts manifest inlib/main.dartdoesn't break, but it's effectively empty (justpart of notechondria_frontend;plus a comment). -
All call sites of
_storeLocalDraft()/_buildOfflineFallbackDraft()renamed to drop the leading underscore. One call site per app (core/note_crud.dart), three apps total.
Files Changed
VERSION— 0.1.79 → 0.1.80.frontend/notechondria_shared/lib/src/app_shell/app_shell_draft_helpers_mixin.dart— new shared mixin (~165 lines, well under the §1.5 cap).frontend/notechondria_shared/lib/notechondria_shared.dart— exportsAppShellDraftHelpersMixin.frontend/editor_app/lib/app_shell.dart— mixin slot + 4 override methods.frontend/editor_app/lib/core/draft_helpers.dart— reduced to documentation shim.frontend/editor_app/lib/core/note_crud.dart— call-site rename.frontend/planner_app/lib/app_shell.dart— same wiring shape.frontend/planner_app/lib/core/draft_helpers.dart— shim.frontend/planner_app/lib/core/note_crud.dart— rename.frontend/portal_app/lib/app_shell.dart— same wiring shape.frontend/portal_app/lib/core/draft_helpers.dart— shim.frontend/portal_app/lib/core/note_crud.dart— rename.docs/TODO.md— mixin entry rewritten "3 of 8" → "2 of 8";AppShellDraftHelpersMixinremoved from the pending list.
Notes
- All four packages (editor / planner / portal / shared) pass
flutter analyze(zero errors) andflutter testsmoke suites. - §1.5 1000-LOC cap respected: largest file in the repo is now
portal_app/lib/app_shell.dartat 971 lines — getting close to the cap from accumulating mixin wiring (planner is at 969). Next round addingAppShellSessionMixinwill need to be careful: it deletesapplyAuthPayload+logoutbodies (~140 lines per app, big shrink) but adds wiring for abstract getters/hooks (~30-40 lines per app). Net should still be a shrink. If it isn't, factor out a chunk of the remainingapp_shell.dartbody into a new extension file. - Behavioral surface unchanged: every call to
storeLocalDraft()andbuildOfflineFallbackDraft()runs the same body it did before. Rename is mechanical; the dedup is in the bodies the mixin now owns. - Mixin order:
AppShellDraftHelpersMixinsits AFTERAppShellLocalPersistMixinandAppShellCourseHelpersMixinin thewithclause. The order doesn't matter semantically (no method-from-mixin overlaps), but keeping a consistent linear order helps readers.