Notechondria

Version: 0.1.78 Build Date: 2026-04-26T23:30

What's Changed

AppShellLocalPersistMixin — fourth shared mixin shipped

Continues the cross-app deduplication kicked off in 0.1.54 (Log), 0.1.55 (AuthActions), and 0.1.56 (OAuth). One of the five mixins listed in docs/TODO.md lands this round; four remain.

  • New frontend/notechondria_shared/lib/src/app_shell/app_shell_local_persist_mixin.dart exposes a uniform persist surface:
    • Read-side getterslocalSettings, localDrafts, localCourses, localStats, persistedUiLogs. Each app's _AppShellState overrides them with => _localX one-liners.
    • Write-side adapterssaveLocalSettings(value), saveLocalDrafts(value), saveLocalCourses(value), saveLocalStats(value), saveLocalLogs(value). Each app overrides them with => _LocalAppStore.saveX(value) one-liners.
    • Concrete persist methodspersistLocalSettings(), persistLocalDrafts(), persistLocalCourses(), persistLocalStats(), persistUiLogs(). Each is a one-liner that wires the read getter to the write adapter. These replace the byte-identical _persistLocal*() extension methods that were duplicated three times.
  • AppShellLogMixin.persistUiLogs (declared abstract since 0.1.54) is now satisfied automatically: AppShellLocalPersistMixin provides a concrete implementation with the matching signature, so each app's _AppShellState no longer needs the Future<void> persistUiLogs() => _persistUiLogs(); forwarder.

Three apps ported

Each app's wiring follows the same shape:

  • _AppShellState mixes in AppShellLocalPersistMixin<AppShell> (slotted between AppShellLogMixin and AppShellAuthActionsMixin in the with clause). Five getter overrides + five adapter overrides in the State class — all one-liners.
  • core/local_persist.dart extension trimmed to just _persistLocalCache (the only persist helper that diverges per app — editor / planner / portal each merge different fields into the cache bucket). The extracted methods are documented in the top-of-file comment so a reader landing here knows where they went.
  • All call sites of _persistLocalSettings() / _persistLocalDrafts() / _persistLocalCourses() / _persistLocalStats() / _persistUiLogs() renamed to drop the leading underscore. Per app: editor 11 files / 42 sites, planner 8 files, portal 8 files. Public name on the mixin, consistent with the existing appendUiLog / log pattern.

What stayed per-app

_persistLocalCache() lives in each app's core/local_persist.dart because the three apps pack different fields into the cache map:

  • editor — front_page + courses + updated_at.
  • planner — courses + activity + planner_events + activity_week + updated_at.
  • portal — front_page + courses + activity + updated_at.

Pulling these into the shared mixin would require a per-app hook returning a Map<String, dynamic> for the merge — net-zero or slightly negative dedup. Left as-is.

TODO.md updated

Cross-app shared mixins — remaining 5 of 8 rewritten to remaining 4 of 8. The four still pending (Session, DraftHelpers, CourseHelpers, HttpClientInternals) each warrant their own round because their abstract surfaces are bigger and the porting is riskier.

Files Changed

  • VERSION — 0.1.77 → 0.1.78.
  • frontend/notechondria_shared/lib/src/app_shell/app_shell_local_persist_mixin.dart — new shared mixin.
  • frontend/notechondria_shared/lib/notechondria_shared.dart — exports AppShellLocalPersistMixin.
  • frontend/editor_app/lib/app_shell.dart — mixin slot + 10 override one-liners; drops the explicit persistUiLogs forwarder.
  • frontend/editor_app/lib/core/local_persist.dart — trimmed to _persistLocalCache + documentation pointer.
  • frontend/editor_app/lib/core/category_actions.dart, core/draft_sync.dart, core/initial_data.dart, core/local_course_builders.dart, core/local_starter.dart, core/local_trash.dart, core/maintenance_actions.dart, core/note_crud.dart, core/settings_actions.dart, core/settings_helpers.dart_persistLocalXpersistLocalX rename (call sites only).
  • frontend/planner_app/lib/app_shell.dart — same wiring shape as editor.
  • frontend/planner_app/lib/core/local_persist.dart — trimmed.
  • frontend/planner_app/lib/core/draft_sync.dart, core/local_course_builders.dart, core/local_starter.dart, core/local_trash.dart, core/maintenance_actions.dart, core/note_crud.dart, core/settings_actions.dart, core/settings_helpers.dart — call-site rename.
  • frontend/portal_app/lib/app_shell.dart — same wiring shape.
  • frontend/portal_app/lib/core/local_persist.dart — trimmed.
  • frontend/portal_app/lib/core/draft_sync.dart, core/local_course_builders.dart, core/local_starter.dart, core/local_trash.dart, core/maintenance_actions.dart, core/note_crud.dart, core/settings_actions.dart, core/settings_helpers.dart — call-site rename.
  • docs/TODO.md — mixin entry rewritten "5 of 8" → "4 of 8"; AppShellLocalPersistMixin removed from the pending list.

Notes

  • All four packages (editor / planner / portal / shared) pass flutter analyze (zero errors; only pre-existing info / lint noise) and flutter test smoke suites.
  • §1.5 1000-LOC cap respected: largest file in the repo is now learner_note_editor.dart at 939 lines (planner / portal each). The new mixin file is 90 lines. Editor app_shell.dart shrank slightly (one fewer override). Planner / portal app_shell.dart grew ~14 lines each from the new wiring (they sit at 934 / 936).
  • Behavioral surface unchanged: every call to persistLocalX() routes to the same _LocalAppStore.saveX(_localX) body it did before. The rename is mechanical; the dedup is in the bodies the mixin now owns.
  • Ordering note: when both AppShellLogMixin and AppShellLocalPersistMixin are in the with clause, AppShellLocalPersistMixin must come AFTER the log mixin because it provides the concrete persistUiLogs() body that satisfies the log mixin's abstract declaration. Dart resolves method-from-mixin via "rightmost wins" — get the order wrong and you'd get back to needing the explicit forwarder.