Commit graph halkyon/AGENTS.md
Author SHA1 Message Date
da51ef73cb
feat(jellyfin): watch from one's own Jellyfin server and learn the words of its subtitles
Animelon's way of learning from video, from the person's own Jellyfin
server: connected per device (Settings, or a studyset's Add material ›
Watch from Jellyfin) with a name and password or a Quick Connect code,
over HTTPS only (plain HTTP only to a server on the same machine); the
token stays in the system's keyring. Shows, films and videos tied to a
studyset follow the person in their settings as the server's and the
item's ids alone (settings schema 9), named by the server when shown.

The Watch screen plays the video (video_player on Android, media_kit's
libmpv on Linux and Windows) with the line in the studyset's front
language cut into the dictionary's words with furigana, and the back
language's line beneath; a word tapped pauses the video and becomes a
card with the line and its translation as its first hint, tagged with
the show. The video's words are listed, most frequent first. The sound is
the track in the studyset's language, whichever the file marks default,
and the menu switches it; the subtitles read are a whole track, never a
forced one (which only covers what the sound doesn't say), and captions
for the hard of hearing only without plain ones. The line and its
translation take the video's width in a box of a fixed height, so the
video keeps its size; in the dark theme the video's colours glow around
it, as YouTube's ambient mode, and the words' panel takes the colour of
the episode's picture under the pointer.

A line is cut into words with one request: DictionaryEndpoint.withKeys
looks up every stretch of it that may be a word, as it stands and as
every word it may be a conjugation of, in the new dictionary_key table
(one row per key, indexed; migration dictionary-keys adds the table and
its index, nothing else), which the loader fills beside the entries.
The instance gets the text of each line looked up and keeps none of it,
as the Watch screen says.

Linux builds need libmpv-dev, and the app links libmpv: the snap stages
Ubuntu's libmpv2, makes the update-alternatives links for BLAS and LAPACK
that ffmpeg's speech recognition needs (staging never runs install
scripts), and removes what the GNOME runtime and core24 already hold, as
older copies of cairo, pango and librsvg hid the runtime's from its GTK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:48 +02:00
61bf7b2fa0
feat(citation): sources are books and articles, websites or YouTube videos
A source was a book's or an article's parts, however a website or a
video had to be squeezed into them. Adding one to a bibliography or a
step now asks what it is, and its form asks for what cites it: a book
or an article its parts as before; a website the page's link, title,
the site's name, authors and year; a YouTube video its link (checked to
be one), title, channel and year. Websites and videos need their link,
marked as required. A video is cited as APA cites one ("[Video]", a
part of YouTube) and plays in the app where it can, else on YouTube.

The kind is kept in the source's JSON, left out for a book or an
article; a kind this app doesn't know reads as a work. The studyset's
payload goes to schema 6 and a step's to schema 4 for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:48 +02:00
1c280157aa
feat(ui): required fields say so with a stronger outline and "Required"
Only a studyset's title was marked, with an asterisk; the other fields
nothing can be saved without said nothing until Save stayed grey. Every
required field now has, while empty, its outline (or its line) stronger
in the primary colour and "Required" below it, before its own helper
text: a studyset's and a path's title (in the sheet and the editor, in
place of its error), a tag's name, a PDF's new name, a YouTube video's
link, a link's address, the handle, an instance's address and a custom
search engine's address. Once written, an empty line keeps its place,
so nothing below moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:47 +02:00
b7e5b695b9
feat(studyset): every menu of a studyset offers all of its options
The library's card offered six options, a topic three, the studyset's
own screen all of them. Each now offers the same, from one list: edit,
move, study options, import and export content, reset progress, archive
or unarchive, delete, more info, and for a copy reporting to its author
and its reports and suggestions. Importing runs where the studyset's
blocs are, so from the library or a topic it opens the studyset's
screen, which asks what to import as it opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:47 +02:00
b70d8e4ed2
feat(studyset): reset progress asks what starts afresh: flashcards, PDFs, paths
Resetting a studyset's progress started its flashcards afresh only. It
now asks what of it starts afresh, with everything inside it, naming
only what has progress and how much: its flashcards, reading its PDFs
(those read some way) and its learning paths (those with a step done),
each ticked to untick. One Undo takes all of it back: reading a PDF
afresh is undone now too, its fresh starts deleted by the moment they
were written, as the paths' are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:47 +02:00
a0b4fa969c
feat(editor): text above is named by the language, shown on hover or a long press
Furigana was the only name for text set above text, though it carries
pinyin, a pronunciation or a translation as well. Its button and dialog
now say furigana in a Japanese studyset, pinyin in a Chinese one, and
text above elsewhere, each with an example of its own: the studyset's
languages reach the core's editors through ContentLanguages, which the
studyset's language scope now sets.

Read-only, the text above shows only under the pointer or after a long
press (another hides it), so the text can be read without it first; its
room stays, so no line moves. While editing, it shows over the pointer
above its dotted text; with the caret inside such text, the button
selects all of it and edits what it has, so it can be seen and changed
without finding its ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:47 +02:00
fcee3c2670
feat(pdf): cards beside a PDF are written in the composer's own editor
The sidebar wrote a front and a back only. It now holds the composer's
editor with a bloc of its own: every kind of card (multiple choice,
vocabulary with Look up, matching, a map, a character), tags, hints and
the explanation, pinning, duplicating and deleting. A new card is tied
to the page shown, which it can be moved to the current one, and waits
for its chapter's tag by name until it is created; text sent from the
pages goes into its front or its answer, as its kind keeps one. Create
saves it once it is complete, as the composer does; going back asks
before discarding what was written.

The screenshot of the pages has a button at the end of the format bar,
outside its scrolling, where an editor's place is for it
(InsertableBlock.inFormatBar); the Insert menu still lists it.

A new card no longer shows twice in the composer when its stored copy
arrives while it is saved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:47 +02:00
72cbec52c2
fix(pdf): the divider beside a PDF follows the pointer, and keeps its place
Dragging it moved the sidebar by each event's step from the width last
built, so whatever arrived between two frames was lost: the sidebar
trailed the pointer, which soon left the 10 dp strip and dropped the
drag. Each move also built every editor of the sidebar anew, its blocks
being a new list on every build. Now the width follows from where the
drag started, the blocks are kept while they stay the same, the handle
is 16 dp with a grip under the pointer, and the resize cursor holds
over the pages while dragging.

The sidebar's share of the room is kept as a device setting once a drag
ends, so every PDF opens with it, in any window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:46 +02:00
ccab43d825
fix(profile): its parts in cards, as Settings groups its own
The Profile ran the account, the inbox and the public profile together
in one list with lines between. Each is now a card of its own on the
lightly tinted background, as Settings groups its parts, and the
statistics' boxes are the same cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:46 +02:00
ec48eaa6cf
fix(app): coming back with a field focused brings its keyboard back
Leaving the app while its keyboard was moving and coming back left the
screen pushed up as if the keyboard were there, the field focused and
no keyboard showing: Flutter gives the window back the keyboard's
height then (flutter#191156, open). Coming back with a field focused now
brings its keyboard back, as other apps on Android do, so the height is
the keyboard's again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:45 +02:00
a57816a869
fix(theme): every spinner is Material 3 Expressive's, pulling down too
The app's spinner is Material 3 Expressive's morphing indicator, but
pulling a list down (the library, a studyset's parts, Explore) showed
Flutter's older circular one, as did the spinners in buttons and a few
dialogs. Pulling down now brings the app's spinner in a small round
container that comes down with the pull and stays while it refreshes
(AppRefreshIndicator, over Flutter's RefreshIndicator without its own
spinner), and buttons show it small in their own colour
(ButtonLoadingIndicator). Only determinate progress (a mastery ring)
stays a circle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:44 +02:00
5455453d20
fix(card): a word's grammar notes grey and slanted in the browser and the dictionary too
A session's answer set each sense's grammar notes (noun, used as a
suffix, …) after its glosses, small, grey and slanted, their meaning on
hover; the card browser listed the glosses alone, and the dictionary's
lists ran the first glosses together. Both now show the senses as the
answer does (VocabularySensesView, out of the study screen into the
card's widgets), the dictionary's rows as one paragraph cut short at two
lines (vocabularySensesSpan).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:44 +02:00
664a966574
fix(card): the browser's title says how many cards, sub-studysets on a line below
The card browser's title ran "Japanese — flashcards (incl.
sub-studysets)". The sub-studysets now go on a small line of their own
below the title, and the title has how many cards there are beside it,
in the badge the material's tabs show their counts in (CountBadge, now
shared).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:44 +02:00
468810c6f4
fix(studyset): the material's tools fold into the menu rather than cutting the tabs off
Where the row beside the material's tabs ran short, Browse, the filter
and the menu kept their room and the tabs were cut off ("Fil…"). The
tools now fold one by one, from the widest to the narrowest that leaves
the tabs their whole width: Browse loses its words first, then the
filter goes into the ⋮ menu, then Browse does too, as the studyset's own
actions fold into icons. A row of its own (CollapsingRow) measures the
tabs and each way of showing the tools and shows the first that fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:44 +02:00
cf6d58aae7
feat(studyset): the material in tabs, each saying how many it holds
The Material part stacked the flashcards, notes and files as sections,
the first three of each behind Show all, as Vaia stacks them. With
hundreds of cards that kept the list a tap away, repeated each list's
tools and pushed the files far down. They are tabs again, Flashcards,
Notes and Files, each with its count, each list whole with its tools at
the end of the tabs' row; Progress and the Study plan stay. The tab is
kept as the part the studyset was left on, while the material shows,
and opened for the first time it opens on a tab with something in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:43 +02:00
ceb7ca1a1e
fix(studyset): Progress is always one of a studyset's parts
Progress joined the switch only once the studyset's reviews were read
and showed something studied, so the switch grew from two parts to
three after a moment. It is now always there, saying there is nothing
learnt yet where nothing was studied, as in Vaia; the switch never
changes as a studyset opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:43 +02:00
959014b4a1
feat(profile): the profile saves as it is written, the handle chosen in a dialog that checks it
Changing the profile meant a Save button at the foot of the form, which
the floating navigation bar half covered on a phone. A field now saves
when it is left or Enter is pressed in it, and a link when it is
removed; "Saving…" and "Saved" show beside the title. The form keeps
what is being written as saves come back, where it used to start afresh
and lose focus after each one.

The handle is chosen in a dialog that asks the instance while it is
typed whether it is free (`ProfileEndpoint.checkHandle`, which tells
nothing `byHandle` doesn't), and choosing the first makes the profile
with what was written. Saves go one at a time, the latest of those
waiting next, and a handle chosen holds for every save after it, so a
field's save can't take it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:43 +02:00
b46dfe22b1
feat(profile): statistics beside the profile, a review counting a minute at most
The Profile tab has two tabs, Profile and Statistics, the latter for
anonymous people too: the time studied in all, per day and the most in
one day, and by days, weeks or months the study time and the ratings as
bars, a tap on a bar saying what it holds. The app draws the charts
itself, as the Progress part's sparkline, rather than taking a chart
package.

A review's time was however long its card was shown: one left on the
screen over lunch counted as an hour of study. It now counts at most a
minute, as Anki does by default, here and in a studyset's Progress; the
stored durations stay as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:43 +02:00
4644125cc4
feat(studyset): deleted studysets wait 14 days in Recently deleted
Deleting a studyset blanked it with everything inside it once a
four-second Undo was over. It now only marks it (`studyset.deletedAt`, a
column as `archivedAt`): it leaves every list, count and session with
what is inside it and waits in the library's Recently deleted for 14
days, saying how many are left, with Restore and Delete now. Deleting no
longer asks first and offers Undo; Delete now asks. Once the 14 days are
over, whichever of the person's devices looks next (as their data opens,
after every sync) deletes it for good, blanking first as before; the
server can't blank a payload and never writes a person's space (ADR
0007). The connector, publishing and the learner count leave deleted
studysets out too.

The migration adds one nullable column: app versions before it stop
syncing until they update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:43 +02:00
11a16f5686
fix(theme): snack bars float above the navigation bar, lined up with it
Flutter's default snack bar is Material 2's fixed one, a rectangle across
the window's foot, which sat squarely on the floating, rounded
navigation bar. Material 3 places snack bars floating above the
navigation bar with small rounded corners: they now stand 8 dp above
it and 16 dp in from the sides, as the bar does, and from medium
windows on they are 560 dp wide and centred rather than as wide as the
window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:42 +02:00
7a2f1a96d9
fix(card): in the browser's detail view the list follows the card shown
Next and Previous changed the card beside the list, but the list stayed
where it was, so the card shown soon lay outside it. The list now
scrolls the card's tile into view: at its end going down, at its start
going up, and near it first where the tile lies too far to be built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:42 +02:00
3181223940
fix(map): a region is picked on a screen of its own, and a country's map offered once it is picked
The composer showed the map inside its scrolling page, so zooming and
scrolling fought over the wheel. A map question's answer is now a field
that opens the map in the whole window, as a studyset's description is
written, with Save in the app bar; nothing scrolls there, so the wheel
alone zooms. The Prefectures chip stood beside East Asia's countries
before Japan was picked; it now shows once Japan, or a prefecture, is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:42 +02:00
51ddd5a5e6
fix(studyset): the parts' switch centred above the material, a section's buttons at its edge
From expanded windows on, the switch between Material, Progress and Study
plan stands centred above the column it switches, rather than at the end
of the path's line; where that column is narrower than the switch, it
ends the line as before. The path takes what is left of the line and
folds before it.

A section's title was Flexible beside a Spacer, which share the free room
between them, so Browse, Reorder and Show all ended halfway across a wide
section. The title now takes what the buttons leave. The study plan's
paths and Explore's licence row had the same row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:36 +02:00
246c9dc143
feat(studyset): Material, Progress and Study plan in place of the tabs
All checks were successful
CI / check (push) Successful in 15m14s
CI / deploy (push) Successful in 1m17s
The studyset's four tabs become three parts, as Vaia has them, chosen
with a switch in the header that ticks as the navigation bar does: below
expanded windows in a line of its own under the title, above the path
and Topics; from there on at the end of the path's line, which the
topics leave free there.

- Material: the flashcards, notes and files one below the other, the
  first three of each with Show all; Browse, the tag filter (where all
  cards show, or while they are narrowed) and Reorder in each section's
  header; an outlined box where a section is empty.
- Progress, once anything in the studyset was studied (a card rated, a
  path finished): how much is learnt (mature cards), how many studied
  cards are due today, and how long it was studied this week on this
  device, with a line of the seven days (StudysetActivityCubit, from the
  reviews' durations).
- Study plan: a ring of the paths finished, Next up (study the cards due,
  go on with the first path not finished), then the paths.

It opens on the part it was left on; a tab kept before counts as its
part. Opened for the first time, it opens on the study plan while a path
is unfinished, else on the material. Material added lands in its part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-12 13:59:56 +02:00
8b9f3174a6
fix(sync): a sync touching many cards no longer blocks the server
serverpod_offline_sync v0.0.10 compares byte payloads byte by byte, each
byte through a UUID parse that throws and is caught: 36 ms for two 2 KB
payloads. A sync whose projection loaded a few thousand card revisions
kept the server's isolate busy for minutes, answering no one, while the
client's method stream idled out after 40 s and sent everything again,
without end.

The shared package now comes from our fork (project-halkyon/
serverpod-offline-sync, branch halkyon/byte-payloads at a signed commit):
v0.0.10 with that comparison fixed, 25 µs for the same payloads. The
override sits in the root pubspec, which the server's Docker build keeps,
so production gets it too. The fix is offered upstream; drop the override
once a release has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-12 13:59:55 +02:00
6b1ff4122e
feat(studyset): a studyset opens on the tab it was left on
All checks were successful
CI / check (push) Successful in 14m17s
CI / deploy (push) Successful in 18s
A studyset opened on its flashcards and slid over to its paths once it
had worked out that one was unfinished, every time it was opened. It now
opens on the tab it was left on, on this device: kept in the person's
folder (studyset_tabs.json), read as their data opens, so at once. The
first time a studyset opens here it still chooses by what it holds
(paths in progress, then flashcards, notes, files), but shows that it is
loading until it knows, rather than one tab and then another; paths that
can't be read count as none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 23:38:02 +02:00
986252fea4
feat(editor): links, and copying and pasting with every format
Text gets a link from the format bar's new link button or with Ctrl+K
(⌘K): the address for the selection, an empty one taking it away, or at
a caret the text as well, written there linked. An address without its
scheme leads to the web. This replaces the editor's own link menu, which
had no button and spoke English only.

Copying and pasting between the app's editors keeps every format: bold,
links, furigana, formulas, callouts, spoilers. The system's clipboard
holds plain text only (the editor's own copy drops the HTML it makes),
so the blocks copied last stay in memory beside it, and a paste takes
them while the clipboard still holds their text; anything else pastes as
plain text, as before, and other apps get plain text. On phones, which
had no way to copy at all, a bar over selected text copies, cuts, pastes
and selects all (the editor's MobileFloatingToolbar), never over a block
taken whole, whose own buttons a tap is for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 23:37:56 +02:00
b8e4e28a08
feat(paths): confetti for a finished path, and an import's progress
All checks were successful
CI / check (push) Successful in 13m32s
CI / deploy (push) Successful in 19s
Finishing a path bursts confetti over the view saying so: pieces in the
palette's brightest colours fall once from the top, turning, and fade
out in under two seconds, so the screen settles. Drawn by hand rather
than with a package; nothing falls with animations switched off on the
device.

Importing paths into a studyset shows its Paths tab at once, and above
the paths, as they come in, how far the import has come: which path of
how many, by its title, and a bar filling with every path and step
stored. Before, the paths appeared one by one in whatever tab showed,
with nothing saying how many were still to come.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 21:45:00 +02:00
192291d35a
fix(editor): a read-only table's rows fit their cells
All checks were successful
CI / check (push) Successful in 15m57s
CI / deploy (push) Successful in 1m22s
A table keeps each row's height on its cells and corrects it after
layout through a transaction, which a read-only editor never applies:
in a path step, a note or a card, a cell of several lines (る, う, つ in
Tae Kim's conjugation tables) ran over its row's borders. Our fork of
the editor now sizes the rows of a read-only table in place, as it
already did the table's height (81c3006c on halkyon/win32-v6; upstream's
main has the same code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 19:50:05 +02:00
277994d31b
fix(editor): tables with whole-number sizes, or none, show
The editor casts a table's sizes (its column width, row height, height,
and each cell's width and height) to decimals, and finds a row's default
height with a null check. JSON written elsewhere may hold whole numbers,
40 where the editor writes 40.0, or no sizes at all: such a table showed
Flutter's error box instead. The Writing System paths in an archive
brought over from elsewhere hold whole numbers. Opening content now gives
its tables their sizes as the editor reads them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 19:50:05 +02:00
ff31652a3a
feat(archive): import a studyset with everything in it and its progress
Your call (2026-10-11): an archive, imported from the Library's Import, to
bring a library from elsewhere as one's own, and later to restore backups
and move studysets between instances before federation.

An archive is a .zip: archive.json says its format; the studyset is laid
out as a release (ADR 0005, rule 3), so the release copier writes it, now
also unlinked from any publication (ReleaseCopier.write), its cards marked
"archive" as imported ones are, handing back each item's new id. A card may
name a dictionary entry instead of its payload: the import makes it from
the instance's dictionary as adding from it does, with today's entry,
examples and tags, and leaves out (counting) one the instance lacks; for
that, DictionaryEndpoint.entries gives entries by their ids, at most 1,000
at once. reviews.json lists reviews of the archive's cards; they go into
the person's review log once the cards are in, so every card's schedule
goes on from them.

The sheet says what the archive holds ("Japanese" holds: 12 studysets,
7,745 flashcards, 3 paths, 20,317 reviews), imports once confirmed, busy
meanwhile, and opens the studyset made. Not an archive, a newer format or
an unreadable one says so and imports nothing. No export into the format
yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 16:51:42 +02:00
47743ced49
fix(shell): the tab screens run on beneath the floating bar
On a phone, a plain band ran behind the floating navigation bar, the
screen's content cut off at its top (your screenshot from the Pixel 9a,
Home's activity card). The bar's whole slot was kept from the screens,
painted in their background: the content stopped above it, as said when
the bar came, until the screens were made ready.

The shell now lets the tab screens run on beneath the bar (extendBody), as
iOS 26's tab bar floats over content. Flutter gives each screen what the
bar covers as the body's bottom padding, and every list of the four tabs
(Home, the library, Explore, Profile) ends with that much room, so its
last item scrolls out from under the bar. Home leaves the Ask field's slot
out while asking is off, as an empty slot would take that room from its
body; with the field, its sections scroll beneath both. A floating button
keeps clear of the system's bar by itself, not of this one: the library's
lifts by what the bar covers (FloatingNavigationBar.coverOf). Inside a
studyset, where the bar steps aside, nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 16:51:37 +02:00
695cc0de2f
feat: a launch screen with the kingfisher and "Halkyon" over "Learn"
While the app opens (the instance, the session, the person's data), it
showed a spinner, in three ways. It now shows the kingfisher, and beneath
it "Halkyon" over "Learn", cobalt and copper as the logo has them, in the
app's own Noto Sans (your call, with JetBrains Mono to compare: the
logo's ITC Avant Garde isn't free to ship), and a quiet line of progress.

AGENTS.md now has the rule on text fields' limits, busy sheets and the
launch screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 15:08:54 +02:00
f9c1f2ae97
feat(studyset): the screens behind a studyset are its path
Deeper studysets, on iOS and Android alike (decided 2026-10-11, after
Material 3's and Apple's guidelines, Android's principles of navigation and
Nielsen Norman Group's on breadcrumbs):

- The screens behind a studyset are its path: below "Japanese › Grammar ›
  Tae Kim's Grammar Guide" lie Grammar's screen, then Japanese's, then the
  library. A topic at any depth opens with the levels between pushed
  beneath it; the path's steps go up by popping to that level's page (they
  replaced the location before, so back could skip levels); a studyset
  opened from elsewhere gets its levels beneath it. Back, Android's
  predictive back, iOS's swipe from the edge and the path all lead one
  level up.
- The path's steps are targets 48 dp high; a long press on back lists the
  levels above, the nearest first and the library last, as iOS's back
  buttons do.
- Below expanded windows the topics open in a sheet from "3 topics ▾" at
  the end of the path's line, where Vaia puts "All topics"; no drawer,
  whose swipe from the edge is iOS's way back and which Material 3
  Expressive deprecates. Topics with topics of their own unfold them, at
  any depth, in the sheet and the side column alike.
- The navigation bar steps aside inside a studyset (its screen, notes,
  paths and dictionary): Material 3 puts it on the top-level screens and
  toolbars below, never both. Apple's guidelines keep the tab bar, lest
  people forget where they are; the path, starting at Library, says where.
  A strip as high as the system's bar keeps those screens clear of it.
- The bar at the bottom keeps Add material and Study.

A trial for review, on its own branch, on top of the floating navigation
bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 15:08:52 +02:00
eee3ff1c5d
feat(shell): the navigation bar floats above the bottom edge on phones
Below medium windows, the navigation bar (Material 3 Expressive's shorter
one) is detached from the window's bottom edge, as iOS 26's tab bar
floats: a rounded bar 16 dp in from the sides and 8 dp above the system's
own bar, at the navigation bar's elevation. Material 3 has no floating
navigation bar for phones, only in its guidelines for headsets.

Around it shows the background of the screen in view: every screen with a
TopGradient claims its background along with its colour (ScreenTint), and
the shell's scaffold takes it, changing as the frame's colour does. The
screens' content stops above the bar; letting it scroll beneath, as iOS
does, would mean padding every screen of the tabs.

A trial for review, on its own branch, on top of the shorter bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 15:08:51 +02:00
f4c502f128
feat(shell): Material 3 Expressive's shorter navigation bar on phones
The navigation bar below medium windows becomes Material 3 Expressive's
flexible one, which Flutter's Material library doesn't have yet: 64 dp
high instead of 80, the label of the tab in view in the secondary colour
(the tokens of Compose's ShortNavigationBar). Its pill stays Flutter's,
64 dp wide rather than 56, as Flutter fixes it.

A trial for review, on its own branch, beside the floating one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-11 15:08:46 +02:00
96c2dc1be8
feat(assistant): search the web too, and choose what goes along
The assistant's field now has a switch at its start: a question goes to
Claude or into a web search, and the field remembers which. A web search
opens the chosen engine's results: on Android the device's own web
search by default (ACTION_WEB_SEARCH, answered by its default browser or
search app; Brave Search where nothing answers), elsewhere Brave Search
by default; Settings › Assistant › Search engine offers Ecosia,
DuckDuckGo, Startpage, Qwant, Google, Bing, or one's own address with
%s where the question goes.

Above the field, chips offer what the question is about: a card's
front, back, each hint and its explanation, the studyset's name and
description. None is chosen, so only what the person picks goes along,
each part cut at 600 characters; Claude gets the question and a compact
line per chosen part, a web search the question and the chosen text,
never the studyset's name. A chosen chip alone is enough to send. The
old sentences around every question (where it was asked, the connector)
are gone: they cost tokens for nothing unless the studyset is chosen.

On a studyset's screen the button now stands above the bottom bar, as in
the Library: unfolded inside the bar, its chips lay outside the bar's
bounds, where taps don't land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 15:11:20 +02:00
5f8fade41c
feat(theme): a studyset's colour on all its screens, Vaia's deeper shades
Every screen of a studyset now washes its colour (its own or the nearest
above it), as Vaia does: studying it, its cards, writing a card, its
notes, its paths and steps, its dictionary. The router hands the colour
down from where it resolves the studyset (ScreenColor), so a screen's
TopGradient without a colour of its own takes it.

The dark tone is now a deep shade of the colour, 42 % of it over black
(cerise a maroon like Vaia's #4e0b23, cobalt a navy), instead of a mix
into the background; light mode keeps its tint. The wash reaches about
as far as the header (160 dp; a studyset 180, Home 360). From medium
windows on, the frame fades into the background 60 % of the way down,
and the wash inside its panel is half as strong, as the frame carries
the colour; screens over the tabs (studying, the card browser and
composer, Settings and Connect Claude) sit in the same frame and panel,
without the rail (TintFrame, FramedScreen). The study session's header
lies on the colour with an edge below it, its status bar styled to suit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 15:11:19 +02:00
d76a49b1c4
feat(theme): the gradient on every main screen, and the rail on its colour
Library, Explore, Profile, Settings and Connect Claude now wash cobalt
down from their top as Home does, as far as their header (200 dp);
Home's wash reaches 360 dp, a studyset's 280.

From medium windows on, the window takes the colour of the screen in
view, as Vaia lays it out: the rail sits on it, transparent and without
a divider, and the screen is a rounded panel inset beside it, with its
own wash at the top. The colour is claimed by the screen's TopGradient
while its tab shows and its route is on top (ScreenTint), so the rail
turns maroon beside a cerise studyset and back to cobalt on leaving it,
in a quarter of a second. The frame is as strong as the rail's labels
allow (WCAG AA on onSurfaceVariant, every palette colour, light and
dark), weaker towards the foot.

Two things the panel's 24 dp less room brought out: a long description
was laid out whole for its first frame, before it was measured, and
overflowed a shorter column; it is now laid out whole but shown no
taller than its preview from the start. And between 840 and about 960 dp
of screen, a studyset's app bar (Study, Add material, Share, options)
didn't fit; Add material is an icon there, its label in the tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 15:11:19 +02:00
56c5b36084
feat(assistant): Claude reads studysets through a connector on the instance
A Claude plan can't drive a chat inside another app, but Claude's apps
(every plan, the phone apps too) can add a remote MCP server as a custom
connector. The instance now is one (ADR 0006): Serverpod's web server
serves the MCP endpoint (/mcp, Streamable HTTP, JSON answers, no
sessions) with five read-only tools over the person's own synced rows
(their studysets, a studyset's cards and notes a page at a time, a
search across cards, a note, their progress), and the OAuth the
specification asks for: protected resource and authorization server
metadata, dynamic client registration, authorization code with PKCE
(S256 only), tokens bound to the resource, iss in the response, refresh
tokens that rotate. Caddy forwards these paths alone to the web server,
which production now runs on 8082.

People connect with a code from the app (Settings › Assistant › Connect
Claude): eight Crockford characters, valid for ten minutes and once,
typed on the authorization page, which names where it returns to, loads
nothing from elsewhere and may not be framed. Anonymous people can
connect too. Codes and tokens are kept only as SHA-256 hashes; an access
token lives an hour, a refresh token 30 days. The app lists what is
connected and disconnects it, which ends its tokens at once; deleting
the account deletes it; a daily job forgets what expired and clients
nobody connected within a week. InstanceInfo says where the connector is
(additive); the migration only adds server-only tables, so devices and
older apps are untouched.

The handlers take plain requests, so the integration tests drive the
whole flow (registration, the code, tokens, refresh, the tools, another
person's content, disconnecting, expiry, pruning, account deletion)
without a web server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 15:11:17 +02:00
f4baf053b3
feat(assistant): ask Claude from Home, the Library, a studyset and a session
A device setting, off unless the person turns it on (Settings ›
Assistant), shows a field at the foot of Home and a button that unfolds
into one: above Create studyset in the Library (smaller), in the free lane
on the right of a studyset's bottom bar (floating alone from expanded
windows on), and in the card's lower right corner while studying.

A question is handed to Claude, not answered here: the app opens
claude.ai/new with the question written in, followed by what it is about
(the person's learning on the instance, a studyset by name, or the studied
card's front and back as text), cut at 12,000 characters, for the person
to read and send in Claude. A Claude plan can't drive a chat inside
another app, as Anthropic allows no third party to offer sign-in with it;
Claude is to read the studysets through a connector on the instance
instead (next). Nothing leaves the app until the person sends it there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 15:11:16 +02:00
7621afaa98
feat(instances): choose the instance, and keep accounts on several
All checks were successful
CI / check (push) Successful in 13m50s
CI / deploy (push) Successful in 1m25s
A new install asks first which instance to keep the person's studysets
on, as Mastodon's apps do: an address, checked to answer as an instance
of Halkyon Learn, or the build's own (learn.halkyon.eu) in one tap.
Nothing is made anywhere before that, not even an anonymous account. An
install from before goes on with the build's instance without asking.

As Moshidon does, the device keeps an account on each instance it was
used with, each signed in with its own folder and session, one in use at
a time. Settings › Switch account lists them: another one switches at
once, anonymous accounts included; Add account chooses another instance;
removing one not in use deletes what the device holds of it. Signing out
ends only the account in use.

The build's own instance keeps its data where installs kept it before
(`people/`, Serverpod's own session key), so they keep everything; other
instances get folders and keys of their own. What the device shares
across instances (its settings, sounds, the Pomodoro timer's place in the
system) is made once and outlives every switch.

The F-Droid listing drops TetheredNet: the app is no longer tied to the
project's instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:50 +02:00
89a3784df4
chore: the website is halkyon.app/learn
Halkyon's website is one site for all its apps, as Proton's is, so
Halkyon Learn's page is halkyon.app/learn rather than a site of its own
at learn.halkyon.app, which redirects there. The snap's and the F-Droid
repository's listings point to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:49 +02:00
0d03ef7973
fix(server): forget the IP addresses of sign-up attempts
Serverpod's rate limit for new anonymous accounts (30 per IP address and
hour) notes every attempt with the address it came from, and deletes
those notes only for email sign-in, which this instance doesn't offer. So
the IP address of everyone who ever started the app stayed in the
database, and in every backup, for good.

The server now deletes them hourly once the limit's hour is past, so an
address is kept for at most two hours. The rows already there go at the
first run after the deploy; backups keep them until they expire (about
three months).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:48 +02:00
ab4542f143
feat(settings): how many changes wait to sync
The Sync card in Settings says how many changes made on this device
haven't reached the server yet: "3 changes wait to sync", in place of
"Everything synced" until a sync takes them, and beside a failure.

The sync engine can't tell, since the server only says what it has at
the start of each sync, so the app counts: every save asks for a sync,
and a sync that goes through takes the saves made before it started.
The count is kept in the person's folder, so it outlives the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:48 +02:00
bc3e8d9ac3
feat(library): who made a studyset, and whether it is public
Each card's foot now says who made the studyset: "By you", or for a copy
of a published studyset its author's handle. Beside the star, a lock or a
globe says whether it is out on Explore, and opens the Publish sheet.

The instance says which of the person's studysets are published and not
withdrawn (`PublishEndpoint.published`, additive: older apps don't call
it), and the app remembers that in the person's folder, so the library
shows it offline too; publishing, withdrawing and looking one up keep it
current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:47 +02:00
fb0ef7cdab
feat(dictionary): the radicals, each listing the kanji written with it
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.

The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:46 +02:00
e04be6603d
perf(export): zip an export with files on an isolate of its own
A studyset with many PDFs took a while to pack, and the window froze
meanwhile. The zip is now made on another isolate (ExportArchiver, an
isolate in the app and in place in widget tests, whose fake clock can't
wait for one), as Anki packages are read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:45 +02:00
c7486a5b1e
feat(study): a kanji's answer shows the studyset's words written with it
A character card's answer lists up to five vocabulary cards of the
studysets the session studies whose front holds the character, with
their reading and first meanings; a tap opens the word's card. The words
come from every card the session reads, whatever tags narrow it, so a
session of kanji alone still shows them, and nothing is fetched: it works
offline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:45 +02:00
a66357ab2a
fix(study): badges count written cards as due on their written schedule
The library's and the topics' counts read every card on its front-first
schedule, while a session with Write by hand or Back first on studies
characters and words on schedules of their own. A card written by hand
showed as due when the session found nothing to study, or the other way
round.

The rule for how a card is studied moves out of the session bloc into
card_study_mode.dart (writtenByHand, asksBack, scheduleModeOf), and
ReviewRepository.schedulesAsStudied gives each card the schedule a
session would study it on; the session and the studyset list both use
it. Home's due forecast still counts front to back, as it reads cards
without their content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:44 +02:00