Commit graph halkyon/halkyon_learn_flutter/test/feature/auth
Author SHA1 Message Date
43b2d86fc0
feat(app): path tiles, Profile and Explore laid out as before
A path shows its route and its secondary title above its title, dimmed
once completed; its options open below the … button, or where the finger
presses for a long press, and resetting its progress asks first. While
the paths are reordered, each keeps its route and secondary title beside
the handle.

Profile sits on the screen itself, a list as wide as readable text,
without a card; Explore on the plain surface too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:18 +02:00
65976ae63a
feat(app): cite sources in APA, MLA or Chicago, and give studysets a bibliography
A source is now shared by learning paths and studysets (core/citation/):
authors as full names, year, title, what it is a part of, publisher,
ISBN, link, and a citation kept as a file brought it. formatCitation
cites it in APA 7, MLA 9 or Chicago 18: it inverts and abbreviates
names itself (particles, suffixes, names written as already cited stay),
sets a work of its own in italics and a part of something apart from
the whole, and writes a missing year as "n.d." in APA and Chicago.

- Settings: a Citations card per device, with a preview and a link to
  the style's own guide.
- Studysets: a Bibliography button opens the works cited, to read and,
  behind its pencil, to change (studyset schema 4).
- Steps: sources edited through the same sheet, opened from an
  "N sources" button in the runner (step schema 2; schema 1 reads on).
- Path import: steps' sources, text kept as written, the old format's
  "citation" read as what a source is part of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:12 +02:00
079179d2ed
Fix what the first Android run found
All checks were successful
CI / check (push) Successful in 6m44s
CI / deploy (push) Successful in 59s
The flow ran on a Pixel 9a and its emulator (2026-10-02); what it found:

- The composer stayed on the card just written after "New flashcard", and
  on the original after "Duplicate": leaving a card changed the state
  before the new one was in it, so the screen took the new card for gone.
  It now waits for the card it asked for.
- Bottom sheets reached under the status bar with the keyboard open
  (useSafeArea).
- The field being written stayed behind the keyboard: the editor scrolls
  only within itself, so the card face now keeps it in view when it opens
  and when the keyboard rises, as a text field does.
- A tap on read-only rich text put a caret in it and opened the keyboard on
  phones, and in the composer never opened the field. RichContentView is
  display only now: no selection, taps go to what surrounds it, a sound's
  button still plays (decided 2026-10-02).
- The card types broke their names on a phone; they are choice chips that
  wrap (decided 2026-10-02).
- The week overview marked days before the person's first activity as
  missed; they show nothing now. An import starts the activity too, without
  counting as studying (decided 2026-10-02).
- A composer preview cut short gave no sign of it; it fades out over its
  last 32 dp, only when cut short (decided 2026-10-02).
- "Isn't on this device yet" showed for any image or sound that failed;
  a file that is here but can't be decoded now says so, and offers no retry.
- The signed-in account card was narrower than the anonymous one.
- The composer's title said "Create flashcards" also when opened on a
  stored card; it says "Edit flashcards in …" then (decided 2026-10-02).
- The Anki fixtures carry a valid PNG and a silent MP3, so they also serve
  to try the import by hand (decided 2026-10-02); regenerated with Anki
  26.9.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-02 19:52:39 +02:00
111532435f
feat(app): the home dashboard: streak, goal, week, activity
Home opens on a study dashboard. The streak's flame sits in the app bar
and opens a sheet of how it stands this week. Below: the study goal (a
ring of the day's or week's progress, set and removed in its sheet), the
week (each day studied, rested, missed or to come with the cards falling
due; days to go back and on; today or a day to come made a rest day),
and a year of activity as a heatmap, its weekdays staying in view while
the weeks scroll. A long press arranges the dashboard: drag a widget
elsewhere, hide or show it; the arrangement is synced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-01 22:17:28 +02:00
8e0f5cf538
feat(app): leaving the profile tab cancels a waiting GitHub sign-in
Decided 2026-10-01, in place of "closing the account dialog cancels it",
which had no dialog left once the account moved into the Profile tab:
choosing another tab cancels a sign-in waiting for GitHub in the browser,
since the person has left it behind. Settings, opened over the profile, is
not leaving it.

Screens learn whether their tab shows from TabVisibility, which the tabs'
container now provides: go_router's indexed stack as before (every tab
alive, hidden ones offstage with their animations paused), each tab wrapped
in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-01 09:59:46 +02:00
b171b8e1d4
feat(app): studyset payload schema 2, stars and the archive
A studyset is now its content (StudysetContent, everything the synced payload
holds) and the columns that change on their own: where it sits (parent,
position), and when it was archived or starred. Like a card's content, the
payload's part is edited as one value; the columns are written one by one,
so starring or archiving on one device and editing on another all survive a
sync (ADR 0004).

The payload's schema 2 adds the secondary title, the colour (a palette name),
the licence (SPDX identifier or name, holder, year) and the languages of the
cards' two sides (BCP 47), each left out when not given. Schema 1 reads as
schema 2 without them, and a field of the wrong kind makes the payload
unreadable rather than half read. Older app versions skip schema 2 studysets,
which costs nothing before the first release; fields foreseeable for the
first release's features are in, so none of them needs a schema 3 later.

The repository stars, unstars, archives and unarchives (at clock time, in
UTC), writing only that column. A new sync test (run against a server)
checks that a star and an archive on one device and a rename on another all
arrive on both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-01 09:59:44 +02:00
c21ce6232b
feat(app): the UI foundation: theme, tabs and routes, strings
The app gets its look and navigation before more features land on them.

- Theme (core/theme/): hand-written light and dark Material 3 colour schemes around
  cobalt and copper; ThemeExtras for the colours Material has no role for (states, the
  four FSRS ratings); the 20 colours a person can give content. Tests hold text to WCAG
  AA contrast on each palette colour and on the dark theme's primary colour and surface.
- Layout and shared widgets: Material 3's window-size classes (core/layout/);
  SectionCard, AppLoadingIndicator, confirmDestructiveAction and FoundById
  (core/widget/). Lucide icons.
- Navigation with go_router: four tabs (Home, Library, Explore, Profile), each keeping
  its own stack of screens, as a navigation rail with the app's icon from 600 dp and as
  a navigation bar below; choosing the open tab again returns to its first screen. A
  studyset's screen sits in the library; studying, cards and settings cover the tabs.
- Routes carry ids: FoundById finds the item in the repository's live list, so a
  studyset deleted on another device, or an old link, says it no longer exists. A
  screen that deletes its own item keeps showing it while it closes, rather than
  flashing that message.
- Every person gets an app of their own (PersonApp, lib/src/app/): a
  MaterialApp.router with its own GoRouter and PersonScope around the navigator, built
  by SessionGate inside the person's keyed subtree. Screens read the person's
  repositories wherever they are built, so the static route() helpers and the listener
  that closed every pushed screen on a change of person are gone: a change of person
  now disposes the whole router and starts again at home.
- The account dialog becomes the Profile tab, with Settings (sign out, delete the
  account, About) behind a gear. A waiting GitHub sign-in now keeps waiting while
  another tab shows; a change of person still cancels it.
- The new screens' strings come from Flutter's gen-l10n (English for now). The
  generated files are committed, left out of analysis and formatting, and written
  anew by `flutter pub get`, so CI's check of generated code also catches stale ones.

Widget tests now start the whole app (test/support/pump_app.dart) at the screen's path
and move through it as a person would, by tabs and taps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-01 09:59:44 +02:00
4ab78d5547
fix(app): GitHub sign-in on Linux and Windows waits for the browser
Signing in with GitHub on Linux ended in "Unable to connect" at 127.0.0.1:43823 after authorizing. flutter_web_auth_2 (v5.1.0, unchanged on its master) ends every pending sign-in when the app is resumed. That suits mobile, where coming back without finishing means giving up, but on the desktop the window regaining focus resumes the app, and the package's loopback server closed before GitHub sent the browser back. FluffyChat's issue #3341 and the package's #171 report the same.

The app now puts its own Linux and Windows implementation behind the package's platform interface (LoopbackWebAuth): the system browser and a loopback address, as RFC 8252 recommends, waiting until GitHub answers, the person cancels, or five minutes pass. It listens on 127.0.0.1 only and answers only the callback's path. Serverpod's PKCE and state check stay in use. There, the account dialog shows "Continue in your browser." with a Cancel button, and closing the dialog cancels the sign-in.

Serverpod (v4.0.3) reports a cancelled browser round trip as an unknown error, so the gateway knows a cancel by having asked for it. A cancel on GitHub's own page, or leaving the browser on Android, still reads as a failure; AGENTS.md says so.

The regression test drives the package's front through a change of focus. With the package's behaviour it fails with the same refused connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 20:38:20 +02:00
a24f89417f
feat(app): delete the account; no one's data left behind
The account dialog offers "Delete account", which asks first and then starts anew with an anonymous account. Signing out of an anonymous account deletes it, and so does switching away from one to GitHub, as decided: nobody could ever sign into it again, so nothing should wait on the server. The confirmations say so.

When a person's data opens, PersonFolders wipes every other person's folder: one person uses a device at a time, so their sessions ended here, whether they signed out, switched, were revoked or deleted. Opening waits for the previous person's database to close, so the files can go even on Windows; if closing hangs, opening goes on after ten seconds and the folder goes next time.

Tests: the repository's sign-out and deletion, the session bloc starting anew after a deletion, the dialog (cancelling keeps the account), and PersonFolders on a real temporary folder. AGENTS.md records deletion, pruning and the folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 20:38:19 +02:00
312b18be5a
feat(app): sign in with GitHub and keep everything
The account dialog offers "Sign in with GitHub" to anonymous people where the instance has GitHub sign-in. It links the GitHub account to the person, who keeps all their studysets. If the account already belongs to another person (typically their account from another device), the app switches to that person right away when this device holds nothing, and otherwise asks first: switch and leave this device's studysets behind, or stay anonymous.

ServerpodAuthGateway uses Serverpod's GitHubSignInService for the browser part (PKCE), with the system browser and a loopback address on Linux and Windows (http://127.0.0.1:43823/github-callback) rather than an embedded webview, and app.halkyon.learn://github-callback elsewhere; Android gets the callback activity. After linking, the device's copy of the session gets the identified scope. A switch signs in through a client without a session, swaps sessions directly (never through "signed out", which would start another anonymous account), then ends the anonymous session left behind.

GitHubSignInBloc holds the flow; tests cover every branch (GitHub sign-in off, linked, cancelled on GitHub, switching right away, asking with staying anonymous as the refusal, switching once confirmed, a failure), and the widget tests the dialog. StudysetRepository gains all(), a one-off read: Stream.first on its stream never completes under the widget tests' fake clock, because the broadcast subscription's cancel future belongs to the root zone; AGENTS.md records it along with the GitHub pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 20:38:17 +02:00
9387e0a827
feat(app): studysets, offline first and synced on triggers
The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs.

SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes.

Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting.

Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 20:38:14 +02:00
158a746ecc
refactor(app): name bloc events and states after their base class
leancode_lint's bloc_subclasses_naming wants every subclass of a bloc's event and state classes to start with the base class's name (SessionEventStarted, SessionStateActive). The rule only applies when the bloc shares a library with them, which is why it stayed quiet while SessionBloc's events and states lived in files of their own. They now share the bloc's file, so the rule guards them, and all blocs follow one convention.

The auth gateway now imports only Client from the generated client: the generated package defines a <Model>Repository class for every table model, whose names clash with the app's own repositories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 20:38:14 +02:00
d6b4c46b57
feat(app): sign in anonymously on first launch
The app's first patterns. Dependencies (lib/src/core/dependencies) is the composition root, built at startup and handed down by DependenciesScope; it replaces the scaffold's global client and restores a stored session before the first frame, so a restart never creates a second anonymous account. AuthGateway with ServerpodAuthGateway is the boundary to the server's authentication, AuthRepository signs in anonymously when nobody is signed in, and SessionBloc (flutter_bloc) keeps a person signed in, also after signing out or a revoked session. SessionGate shows a spinner while signing in, offers to try again when that fails, and builds the app anew for every person. AccountButton explains that progress is saved on this device and asks before signing out of an anonymous account, which can't be restored. AppBlocObserver logs all blocs.

Tests: the repository, the bloc's sequences (a stored session is kept, a failed sign-in is retried, signing out starts anew), and the widgets, including that cancelling the sign-out keeps the account. test/support holds the fake gateway and testDependencies(). AGENTS.md describes the composition root and the test factory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 20:38:13 +02:00