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.dartexposes three byte-identical course helpers as concrete mixin methods:isLocalCourse(course)— true when the course Map hasis_local_course: trueOR a negative id. Returns false for null courses so callers can pass_selectedCoursewithout an explicit null check.decorateRemoteCourse(course)— annotates a freshly- decoded remote course withis_local_course: falseand a computedis_ownedbased oncurrentUsernamematching the course'sowner.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 bydecorateRemoteCourseto computeis_owned. Each app's_AppShellStatereturns_profile?['username']?.toString().List<Map<String, dynamic>> get localCourses— used byfrontPageFallbackPayloadfor the empty-remote fallback. Same getter shape asAppShellLocalPersistMixin.localCoursesso a single override on the State satisfies both mixins.
What stayed per-app
_chooseDefaultCourse— editor + portal take afrontPageparameter (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 nullfrontPagethrough planner, neither of which justifies the dedup. Stays in each app'score/course_helpers.dart._localNotesForCourse(editor only) — uses_decodeNoteMetadatawhich is private to editor. Stays where it is.
Three apps ported
Each app's wiring follows the same shape established in 0.1.78:
-
_AppShellStatemixes inAppShellCourseHelpersMixin<AppShell>(slotted betweenAppShellLocalPersistMixinandAppShellAuthActionsMixinin thewithclause). One new getter override on each app —currentUsername— added next to the existing local-persist mixin overrides. -
core/course_helpers.dartextension trimmed to just_chooseDefaultCourse(and_localNotesForCourseon 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— exportsAppShellCourseHelpersMixin.frontend/editor_app/lib/app_shell.dart— mixin slot +currentUsernamegetter 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";AppShellCourseHelpersMixinremoved 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 941 lines (grew slightly from adding the new mixin slot + getter override). Editor / planner / portalapp_shell.dartall 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:
AppShellCourseHelpersMixinsits AFTERAppShellLocalPersistMixinin thewithclause so itslocalCoursesrequirement 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
(
_handleDestinationSelectedunused,surfaceVariant/withOpacitydeprecation) carried over from earlier rounds — not addressed this round.