Notechondria

Version: 0.1.79 Build Date: 2026-04-27T00:00

What's Changed

AppShellCourseHelpersMixin — fifth shared mixin shipped

Continues the cross-app deduplication started in 0.1.54 (Log), 0.1.55 (AuthActions), 0.1.56 (OAuth), and 0.1.78 (LocalPersist). One more mixin from docs/TODO.md lands; three remain (Session, DraftHelpers, HttpClientInternals).

  • New frontend/notechondria_shared/lib/src/app_shell/app_shell_course_helpers_mixin.dart exposes three byte-identical course helpers as concrete mixin methods:

    • isLocalCourse(course) — true when the course Map has is_local_course: true OR a negative id. Returns false for null courses so callers can pass _selectedCourse without an explicit null check.
    • decorateRemoteCourse(course) — annotates a freshly- decoded remote course with is_local_course: false and a computed is_owned based on currentUsername matching the course's owner.username (case-insensitive). Idempotent.
    • frontPageFallbackPayload(remoteCourses) — synthesises a plausible front-page payload from the first 3 remote courses (or local courses when remote is empty). Used during initial boot before the server /front-page/ payload arrives. Editor and portal both call it; planner doesn't have a front-page surface.
  • Two abstract getters the implementing State must override:

    • String? get currentUsername — used by decorateRemoteCourse to compute is_owned. Each app's _AppShellState returns _profile?['username']?.toString().
    • List<Map<String, dynamic>> get localCourses — used by frontPageFallbackPayload for the empty-remote fallback. Same getter shape as AppShellLocalPersistMixin.localCourses so a single override on the State satisfies both mixins.

What stayed per-app

  • _chooseDefaultCourse — editor + portal take a frontPage parameter (consulting the server's stored default course); planner doesn't have a front-page concept and uses a 2-arg signature. Sharing across all three would require either diverging the mixin signature or threading a null frontPage through planner, neither of which justifies the dedup. Stays in each app's core/course_helpers.dart.
  • _localNotesForCourse (editor only) — uses _decodeNoteMetadata which is private to editor. Stays where it is.

Three apps ported

Each app's wiring follows the same shape established in 0.1.78:

  • _AppShellState mixes in AppShellCourseHelpersMixin<AppShell> (slotted between AppShellLocalPersistMixin and AppShellAuthActionsMixin in the with clause). One new getter override on each app — currentUsername — added next to the existing local-persist mixin overrides.
  • core/course_helpers.dart extension trimmed to just _chooseDefaultCourse (and _localNotesForCourse on editor). Top-of-file comment documents which methods moved and where.
  • All call sites of _isLocalCourse() / _decorateRemoteCourse() / _frontPageFallbackPayload() renamed to drop the leading underscore. Per app: editor 7 files, planner 5 files, portal 5 files. Public name on the mixin, consistent with the existing pattern.

Files Changed

  • VERSION — 0.1.78 → 0.1.79.
  • frontend/notechondria_shared/lib/src/app_shell/app_shell_course_helpers_mixin.dart — new shared mixin (~115 lines).
  • frontend/notechondria_shared/lib/notechondria_shared.dart — exports AppShellCourseHelpersMixin.
  • frontend/editor_app/lib/app_shell.dart — mixin slot + currentUsername getter override.
  • frontend/editor_app/lib/core/course_helpers.dart — trimmed to _chooseDefaultCourse + _localNotesForCourse + comment.
  • frontend/editor_app/lib/core/load_local_state.dart, core/initial_data.dart, core/local_course_builders.dart, core/category_actions.dart, core/note_loading.dart, core/build_helpers.dart, core/local_starter.dart — call-site rename (no leading underscore).
  • frontend/planner_app/lib/app_shell.dart — same wiring shape.
  • frontend/planner_app/lib/core/course_helpers.dart — trimmed to 2-arg _chooseDefaultCourse.
  • frontend/planner_app/lib/core/initial_data.dart, core/load_local_state.dart, core/maintenance_actions.dart, core/local_course_builders.dart, core/note_loading.dart — call-site rename.
  • frontend/portal_app/lib/app_shell.dart — same wiring shape.
  • frontend/portal_app/lib/core/course_helpers.dart — trimmed to _chooseDefaultCourse.
  • frontend/portal_app/lib/core/local_course_builders.dart, core/maintenance_actions.dart, core/initial_data.dart, core/load_local_state.dart, core/note_loading.dart — call-site rename.
  • docs/TODO.md — mixin entry rewritten "4 of 8" → "3 of 8"; AppShellCourseHelpersMixin removed from the pending list.

Notes

  • All four packages (editor / planner / portal / shared) pass flutter analyze (zero errors) and flutter test smoke suites.
  • §1.5 1000-LOC cap respected: largest file in the repo is now portal_app/lib/app_shell.dart at 941 lines (grew slightly from adding the new mixin slot + getter override). Editor / planner / portal app_shell.dart all remain well under the cap (787 / 939 / 941).
  • Behavioral surface unchanged: every call to isLocalCourse(), decorateRemoteCourse(), frontPageFallbackPayload() runs the same body it did before. The rename is mechanical; the dedup is in the bodies the mixin now owns.
  • Mixin order: AppShellCourseHelpersMixin sits AFTER AppShellLocalPersistMixin in the with clause so its localCourses requirement is satisfied by the same override that satisfies the persist mixin's. Order doesn't matter semantically here (both require the same getter and neither provides it concretely), but keeping a single linear order helps readers.
  • Pre-existing unrelated warnings on planner/portal app_shell (_handleDestinationSelected unused, surfaceVariant / withOpacity deprecation) carried over from earlier rounds — not addressed this round.