Questions asked from the app went to Claude alone. Settings › Assistant
now chooses where they open (decided 2026-10-14): Lumo, the default, by
its guest chat with the question in the address's fragment (Lumo's
`prefill`, which needs no account and reaches no server; the address for
one's own account loses the question when nobody is signed in), ChatGPT,
Claude, Mistral, Perplexity, Duck.ai, Grok, Kagi Assistant, HuggingChat,
or an address of one's own with %s. Each link fills the question in for
the person to send (Perplexity answers at once, Grok asks first); Gemini,
which has no such link, and Copilot, whose stopped working, are left out.
The field names the assistant ("Ask Lumo…"). A device that sent questions
to Claude goes on asking an assistant, now Lumo. Connecting Claude to read
studysets stays as it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Stage 4 of the public tier (ADR 0005, rule 6), its server and the app's
data. A learner writes a report or a suggestion on a published studyset,
a card, note or step of it, in their own synced space, so it works
offline; replies and the author's decisions (closed, hidden, accepted,
declined) likewise, in the writer's or the author's space. The instance
gathers them: public to everyone signed in, but for what the author
hides; for the author's inbox and the writer's. What counts is decided
as rows are read, since a modified app could write anything: threads
from people with an identity and a public profile the author hasn't
blocked, replies from the writer and the author alone, decisions from
the author alone, no suggestion under "no derivatives". The author gets
the files a suggestion names from its writer's, and the next release
credits the writers of the suggestions accepted since the last
(Co-authored-by). Deleting a person deletes their blocks and what they
read; what they wrote goes with their space.
In the app, the repository writes reports and suggestions (a card, note
or step as the learner has it, without what is theirs alone, with the
files it names) and takes an accepted suggestion into the author's own
card, note or step.
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>
A copy's top studyset says when a newer release of what it came from is
out, and what it would bring: changed, new and withdrawn items, counted
against the release the copy last took. Update takes it, never by
itself (ADR 0005, rule 4): cards, notes and steps take the release as a
revision on the one where they last took a release and merge per side,
as a dictionary's corrections do, so what the learner changed stays and
a side both changed is a conflict to settle; studysets, paths and PDFs
take each field the learner left as it was. New items come in where
they belong, unless the learner deleted that part; what the author
withdrew stays, marked, and the card browser filters for it; what the
learner deleted stays deleted; progress is never touched.
The instance gives out any release by its version, for the app to
compare the two. UpdateTaken moves into the shared revision merge, now
that notes and steps take updates too.
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>
Explore lists what is published on the instance as the old app's feed:
search over titles and authors, filters for languages and kinds of
content, the newest release first, the next page only when asked for;
each studyset opens a summary with all of it. Anonymous people explore
too.
Add to library makes a copy in the app, as an import: through the
person's own repositories, the files among their own files, the tags by
name among their own tags, everything read first, so a release in a
schema this app doesn't know is refused whole. The server never writes
into a learner's space. Every item of a copy names where it came from
(publication, item, release: card schema 9, note 3, step 3, path 2,
PDF 2, studyset 5), merged per side like the rest, for later releases to
be taken from there.
The Publish sheet takes a studyset off Explore again, after asking.
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>
A person with an identity and a public profile publishes a studyset with
everything inside it as its next release (ADR 0005, rule 3): the server
reads their synced rows and writes them through the git command as a
commit on main and the annotated tag vN, in a bare repository of the
studyset's own, named by a random public id made at the first publish.
A release is a directory per studyset with a file per card, note, path,
step and PDF (its id, position, tag names and payload as synced, keys
sorted and indented) and the files the content names, without what is
only the author's (a card's report, a PDF's deadline). Publishing is
refused without a licence allowing passing it on in every studyset ("no
derivatives" allowed), with revisions to settle, with files not uploaded
yet, when nothing changed, or for a name that won't do; the name is
fixed from the first release, as is the person's handle.
In the app, Share in a studyset's action row opens the Publish sheet;
publishing syncs first, and settles by itself the revisions that merge
cleanly, listing only real conflicts. The server's image gets git, the
repositories a volume of their own, and the hourly backup a git bundle of
each.
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>
Stage 1 of the public tier (ADR 0005). A person with an identity has a
public profile: a handle by Mastodon's rules (1 to 30 letters, digits or
underscores, unique whatever its case, a few kept for the instance),
changeable until they first publish, on the instance's account domain
(ACCOUNT_DOMAIN, else the server's host; the instance info names it), so
`raisondetredev@halkyon.eu`; a display name, a bio, up to four links and
an avatar. The table is server only: the app gets a Profile without the
person's id. ProfileEndpoint (identified only) reads, saves and removes
the caller's own; ProfilesEndpoint finds anyone's by handle.
An avatar is a PNG of at most 1 MB that the app draws anew from the
picture chosen, its middle square at 400 pixels, so a photo's metadata
never leaves the device. The server keeps it in the public storage named
by its content, so its URL tells nothing about whose it is, and deletes it
once no profile shows it. Deleting a person deletes their profile and its
picture.
In the app, the Profile tab holds the profile as a form, laid out as the
old app's; anonymous people are told GitHub sign-in gives them one.
Migration 20261009184924904-public-profiles creates public_profile with a
unique index on the person and one on the handle in lower case; it touches
no other table, and the old server never sees 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>
KanjiVG's strokes come with the app (assets/kanjivg/, CC BY-SA 3.0, its
notice in the licence list): 94 gzipped files, one per 256 code points,
2.7 MB in all, a file read the first time a character of it is asked for
(StrokeRepository, app-wide beside the maps). The strokes are SVG path
data, read with path_parsing (the Flutter team's, pure Dart, already in
the build through flutter_svg), so the model can flatten them into points
without dart:ui, as checking handwriting will.
A character's sheet traces its strokes in order over those to come, with
its stroke count, meanings, readings, level and the components KanjiVG
splits it into, each a tap away. It opens from a studied character's
answer (Stroke order) and from a character found in the dictionary.
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>
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
Export content, in the studyset's options, writes the studyset and
everything inside it as one readable Markdown document (decided
2026-10-08: no re-importable archive before the public tier's release
files): its title, secondary title, description and licence, then its
flashcards (answers below their questions, choices as task lists),
notes, learning paths (steps with their kinds, answers, feedback and
sources), PDFs and bibliography, then each sub-studyset a level deeper.
Rich text keeps its formatting; formulas go between dollars, callouts
become quotes, spoilers HTML details, YouTube videos links. With files
(images, sounds, PDFs) it keeps a .zip of the document and them under
files/; a file this device can't have is named and counted.
Saving goes through a FileSaver: file_selector's dialog on the desktop,
file_picker's (Android's Storage Access Framework) on phones, a direct
dependency now at the version the editor already brings.
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>
Deleting a card, from the list or while studying, asks first ("Delete
flashcard?"), then offers Undo for a few seconds ("Flashcard deleted").
Meanwhile the card leaves every list and session; once the time to
undo is over, its revisions are blanked and it is deleted for good, as
notes and studysets already are (CardDeletions, a DelayedDeletions).
Undo while studying puts the card back into the round where it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Home has a Pomodoro timer (decided 2026-10-07, after Goodtime): the
profile it runs, chosen there while no timer is set, the time left of
the phase, and Start, Pause or Resume, Skip and Stop. While studying, a
small countdown sits in the study screen's header, and a tap pauses it
or goes on. A phase that ends leads to the next, which starts by itself
where the profile says so and otherwise waits, paused at its full
length.
Profiles follow the person to every device, in their settings (schema
4, still written as schema 3 while the profiles are as they come, so
older apps go on reading the settings): Classic, 25 minutes of focus,
5 of break and 15 after four, and 52/17 without long breaks, built in;
both can be changed, more added and deleted, from Home's card or from
Settings. A profile can start breaks and focus by themselves, and keep
the device quiet while focusing. The running timer is the device's own,
kept in the person's folder: closed and opened again, the app catches
up with the phases that ran out meanwhile.
Android: the app's own foreground service (Kotlin, specialUse, as
Goodtime does) shows an ongoing notification counting the phase down,
with Pause or Resume, Skip and Stop; on Android 16 it is a Live Update,
its progress bar the profile's cycle, each phase a segment, focus in
cobalt and breaks in copper. An exact alarm (USE_EXACT_ALARM) alerts
with sound and vibration when the phase ends, also with the app
closed, and Do Not Disturb goes on while focusing once the person
allowed it, the filter before given back afterwards. No notification
plugin: flutter_local_notifications has Live Updates only in a
prerelease that needs compileSdk 37; androidx.core 1.18, which the
build resolved already, has every API needed.
Linux, over the session bus (package:dbus, already in the build): where
the desktop shows icons in its top bar (GNOME with the AppIndicator
extension, which Ubuntu has built in; KDE) the timer counts down there,
the buttons in the icon's menu, as a StatusNotifierItem written in Dart
on the names and paths the snap's desktop plug allows; elsewhere a
notification with the buttons says until when the phase runs. An alert
with sound ends each phase while the app runs, and GNOME's banners stay
off while focusing, through gsettings. xdg-desktop-portal's Background
status, the planned fallback, serves Flatpaks only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Settings gets a card for how this device shows the app: its appearance
(as the device, light or dark) and its language (the device's, or one
the app speaks, each named in its own language), side by side from
medium windows on, as two outlined dropdowns. Both stay on the device
rather than following the person: they are kept in
<app support>/device_settings.json (written to a temporary file and
renamed into place), read before the app shows, so its first frame
already looks as chosen, and a file that can't be read falls back to
what the device does. DeviceSettingsBloc sits above SessionGate's keyed
subtree, since the settings outlive every person, and both the plain
app and the person's app take their theme mode and locale from it.
At the bottom of Settings, the release and its build, read once at
start together with the build the server is asked about.
Home has a Settings cog beside the streak, with the sync trouble dot as
on Profile. The library's create button reads "Create studyset", and
Delete is red in every menu that offers it: the studyset's card, screen
and rail, and the composer's draft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Published apps can't be made to update, so the app now notices when it
no longer matches the server: the server names the oldest app build it
works with (instance.info's minimumAppBuild), asked before the first
sync of each launch, and the sync engine's refusal of differing synced
tables counts the same. Either way the app says "update the app; if it
is up to date, the server needs an update" in a banner on Home and in
the new Sync card in Settings, and what the person does meanwhile stays
on the device.
The Sync card shows where sync stands since the app started (syncing,
everything synced, sync failing, with the last completion and failure)
and offers Sync now. While the last sync failed, a dot sits on the
Profile tab and the Settings cog. One sync that goes through clears it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
PDFs live in a studyset's new Files tab: uploaded from Add material
(a file the studyset holds already asks first), each row with its
pages, cards, notes and size, how far it is read and its reading
deadline; renamed, given a deadline (a page or a chapter by a day),
marked as read, started afresh, deleted with a moment to undo.
The reader (/pdfs/:id) opens where reading left off: on wide windows
a rail of tools (search, fit, zoom, fullscreen), a page counter with
the chapter, and a sidebar; on phones a top bar, a page scrubber and
a sheet. The sidebar holds the PDF's flashcards, by page or chapter,
made from selected text or written there, each tied to its page and
tagged with its chapter; and the page's note, an ordinary note tied
to the page. Screenshots of a region become images that know their
page. Cards and screenshots lead back to their page; reading on past
a chapter with cards suggests studying it. Home shows the deadlines
running and to come, and a timeline of them.
Server: pdf_source rows (content in the payload, last writer wins)
and an append-only pdf_reading log, the furthest page derived from
it so devices never conflict. Files up to 50 MB for PDFs (20 MB for
others; a larger file that isn't a PDF is deleted once stored). A
deleted PDF takes its file's asset row along unless another PDF has
the same file. Card payload 4 and note payload 2 keep the page.
The reader draws with pdfrx (PDFium) behind PdfEngine in
Dependencies; widget tests use a fake, and
integration_test/pdf_engine_test.dart checks the real engine on a
device or the desktop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Blocks beyond the editor's own, each built once for editor and view:
- callout: eight kinds in theme colours, kind, title and "folded at
first" from its header; written in place, left with Enter on its
empty last line; never inside another
- spoiler: written in plain sight, hidden behind Reveal read-only
- formula on its own line, written on its own screen with a preview
and snippet buttons, and formulas within a line (Σ in the format
bar); set by flutter_math_fork behind MathView, LaTeX's numbered
environments read as their aligned forms
- image and sound picked from the device, kept as synced files
- YouTube video from its link, with its start and a caption: a card
until played, nothing fetched before; played in place where a web
view exists (youtube_player_iframe, privacy-enhanced), else opened
in the browser, behind YoutubeVideos in Dependencies
- study button: a studyset, a tag or all cards, with or without its
topics, back first; its StudyFocus rides on the study route and
narrows that session only
- tables, already in the editor, now insertable everywhere
An Insert button opens on the format bar, so blocks can be inserted on
phones too; the desktop's / menu lists the same blocks, translated.
Card faces now dock the format bar while written, so cards get blocks
and formatting on phones as well. The blocks are listed in
EditorBlocks: the core's, plus the features' through
EditorBlocksScope. Blocks of a type the app doesn't know say they need
a newer version and stay as they are, so formatVersion stays 1.
Their types and keys are in content_blocks.dart, which RichContent
reads: a block's text counts as text, and a video or study button
isn't blank. The path import now makes formulas real ones.
integration_test/youtube_player_test.dart plays a real video through
the app's player on a device, which no widget test can: YouTube
refuses embeds that don't say where they are embedded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Server: five synced tables, only additions (migration
20261002211346701-learning-paths):
- `learning_path`: its studyset, a fractional position, and its own
content in the payload {title, secondaryTitle, description}, the last
writer winning, as a studyset's;
- `learning_path_step`: its path, a position, and its kind ('info',
'singleChoice', 'multipleChoice', 'dragAndDrop', 'fillInTheBlank'),
fixed once created;
- `learning_path_step_revision`: a step's content as append-only
revisions, as a card's: {title, body, exercise, rightFeedback,
wrongFeedback, sources};
- `learning_path_answer`: an append-only log (right, wrong, continued,
reset), as reviews are, so answers from two devices never conflict;
- `learning_path_tag`: one row per assignment, as `card_tag`.
App, `feature/learning_path`: the five exercises and their JSON, grading
that marks only the learner's own wrong picks, progress worked out from
the log (a step is done once answered right since its last fresh start),
`LearningPathRepository` (steps saved on what they started from, merged
per field) and `PathProgressRepository` (answers, resets with Undo, the
times steps were done). Paths share the person's tags; deleting a path
or a step can be undone for a moment, and a studyset's deletion takes
its paths. Tests: models, codecs, repositories, tags, the device
migration guard, and sync tests against the server (four).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Server: three synced tables, only additions (migration
20261002191122122-notes): `note` (its studyset, a fractional position),
`note_revision` (append-only, as a card's: parents, author, report,
the payload {schemaVersion, title, body}) and `note_tag` (one row per
assignment, never updated, as `card_tag`). Each indexes `spaceId`, and
everything goes with its space, studyset, note or tag. A migration of
synced tables locks app builds from before it out of syncing (ADR 0002,
rule 3): cheap now, before the first release.
App, `feature/note`:
- `NoteContent` is a title and rich text, merged apart (ADR 0003): a
title changed on one device and the body on another both survive, a
body changed on two is a conflict.
- `NoteRepository` creates a note at the end of its studyset, saves an
edit on the revisions it started from (`saveEdit`, as the card
composer now does), says when a note was last edited (its newest
head's UUID v7), counts notes per studyset, and deletes a note's
revisions blanked first (ADR 0002, rule 2).
- Notes share the person's tags: `setTagsOfNote`, and folding tags of
one name or undoing a tag's deletion covers notes too.
- The delayed deletion with its Undo moves to `core/async`
(`DelayedDeletions`): `StudysetDeletions` builds on it and now takes
a studyset's notes too; `NoteDeletions` deletes a single note.
Tests: content, merge, payload, repository, deletions, tags, the device
migration guard, and sync tests against the server (four).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Files in content: the person's images and sounds as asset rows synced like
all content, their files kept by hash on each device, uploaded after each
sync, downloaded on demand and checked against their hash
(core/asset/AssetRepository). Rich text names them as asset:<id> URLs, in
the editor's image block and an audio block of ours; any other URL is never
fetched. Sounds play through audioplayers, one at a time. Content holding
only an image or a sound isn't blank, and lists show it as such.
The Anki import ("Import from Anki" under Add material): .apkg and .colpkg
packages in all three formats Anki has written, read on an isolate of their
own. Modern packages compress everything with zstd in frames that don't
record their size, which pure_zstd v1.0.0 refuses; our fork decodes them.
Fields become rich text by a converter of our own (lines, lists, formatting,
images, sounds, cloze deletions hidden on the front). One card per note,
the files first, studysets as chosen (flatten, sub-studysets, or tags by
subdeck), the notes' tags, and, if wanted, the review history, which FSRS
replays: the first template's reviews to the card's schedule, a reversed
note type's second to the reverse one.
Imported cards say where they came from in their first revision (card
schema 3); the dashboard doesn't count them as written. The test fixtures
are exported by Anki itself (tool/anki/make_fixtures.py).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
What the dashboard shows, worked out from this device's data: cards
rated (crammed included, fresh starts not) and cards written per local
day; the streak, up to today or yesterday, rest days counting; the cards
falling due per day, but those resting in archived studysets; the study
goal's progress in its day or week. The goal, the rest days and the
dashboard's arrangement live in the synced settings (payload schema 3).
Cards tell when they were made by their UUID v7 ids; reviews report
their changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A map question is a multiple-choice card drawn on a map: the composer's
Map kind takes the region tapped as its answer, and studying it means
tapping a region and checking, the right one green and a wrong pick red,
each named. The map of Japan's 47 prefectures comes from Natural Earth
(public domain), built by tool/maps/japan_prefectures.py from release
v5.1.2 (its download checked by hash): simplified, small islands
dropped, rounded; about 107 KB. Maps are drawn to fit, tappable and
zoomable for small regions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The study screen's session settings choose tags to study a studyset by:
its cards with any of them. The choice lives in the person's settings
row, synced, by tag id and name, so a renamed tag stays chosen and so
does one folded into another of its name. Two settings rows made offline
fold into the one with the smallest id, which gets what only the other
has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A tag is its name: names are lowercase, a person has one tag per name,
and two of one name (made offline on two devices, or by renaming) fold
into the one with the smallest id, so every device ends up alike. The
colour lives in the payload. Deleting a card deletes its tags too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01. Deleting a studyset from its own screen closes that
screen, which would have dropped a deletion kept there, and the studyset
would have quietly stayed. StudysetDeletions, opened with the person's data,
now keeps every deletion that can still be undone: a deleted studyset leaves
every list at once, and once the time to undo is over the whole subtree is
deleted, what is inside first, each studyset's cards blanked before it. It
closes first with the person's data: what still waits is dropped and stays,
what is under way finishes while the repositories are open. A failed
deletion shows what is left again.
The library's bloc hands its deletions to it and leaves out what it reports
as pending, instead of keeping timers of its own. Tests that follow a real
timer wait for the outcome (eventually) rather than for a fixed time a busy
machine might not keep.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset's card list gains "Study". The study screen shows the front, then the answer and the four ratings, each labelled with when the card would come back; every rating is recorded with how long answering took, and the queue is worked out anew after each.
Schedules are never stored: scheduleOf replays a card's reviews in time order with FSRS-6 (fsrs v2.0.1, pinned exactly so every device on one app version computes the same). The package's fuzz can only be seeded through an API meant for tests, and due dates don't feed back into a card's state, so the replay runs without fuzz and fuzzedDays spreads the final due date over FSRS's range, picked by a stable FNV-1a fraction of the card's id and review count. studyQueue takes due cards earliest first, then new cards in studyset order, 20 per studyset and local day, and brings a learning card due within 20 minutes early when nothing else is due. Deleting a card deletes its reviews.
Tests: the replay (learning, growing intervals, the same schedule whatever order reviews arrive in, previews matching what a rating yields), the fuzz range, the queue (order, the daily limit counted per local day, learn-ahead), the repository, the session bloc under a controlled clock, the screen, and a sync test for ratings on two devices. AGENTS.md records the review log and scheduling, and how plugin lint rules are ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The account dialog opens Flutter's about dialog: the licences (AGPL-3.0-or-later for the app, SSPL-1.0 for the server), the address of the source the instance names, as selectable text, and "View licenses" with every package's notice, which the BSD and MIT packages require in a distributed app. The address comes through a small about feature (gateway, repository, bloc) that says so when the instance can't be reached. Tests cover the bloc both ways and the way from the account dialog; AGENTS.md records the obligation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset's screen lists its cards (reorderable; a marker on cards changed on two devices) and opens a card's screen with an editor for each side. Saving appends a revision whose parents are all current heads. mergeRevisions merges several heads per side against their newest common ancestor, as decided: different sides merge automatically, the same side changed differently is a conflict. The card screen then shows every version of that side with "Keep this", and saving is possible once each conflicted side is settled, which resolves the conflict for every device. Nothing is merged silently inside a side.
CardRepository watches a studyset's cards, creates at the end and moves by fractional index (fractional_indexing_dart, checked against the reference algorithm's test vectors), and deletes by blanking every revision first (ADR 0002, rule 2). Deleting a studyset now deletes its cards the same way before the studyset goes. Every person gets a CardRepository whose revisions name them as author. RichContent gains plainText for the list's previews.
Tests: the merge (chains, different and same sides, conflicts, resolving, three heads, missing parents, arrival order), the payload, the repository against a fake gateway, both blocs, and the widgets, including that cancelling a delete keeps the card. The sync tests, which need the running server, cover merging and conflicts across two devices and blank revisions on the server; their device helper is now shared in test/sync/support. AGENTS.md describes the revision pattern, the fractional order, and two tooling traps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Two guarantees of step 7 had no test. An open studyset closes when the person changes (here: the server revokes the session, and the app starts anew with another anonymous person), and the next person sees none of it. PersonScope shows the person's app once their data is open, says so when it can't be opened and then leaves no unhandled error behind (checked by removing the handler: the test fails), and closes the person's data when it goes away.
testDependencies takes an optional openPerson for this, and the fake auth gateway can revoke the session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
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>