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>
An Anki import switches to Flashcards when the cards come into this
studyset (flattened or tagged by subdeck), not when subdecks become
sub-studysets; an imported path switches to Paths. Creating flashcards,
a note or a path switches to its tab as well, seen on coming back.
On opening, the studyset shows the first tab with content of its own:
unfinished paths, then flashcards, then notes; Flashcards when it holds
nothing. Its topics' content doesn't count, since the tabs list only the
studyset's own. A tab tapped or swiped to by the person, or material
added, stays.
Add material is built both in the app bar and in the bottom bar, so a
ContentTabCubit carries the requests to the tab controller, and
importFromAnki now returns the mode it imported with.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
- A Paths tab before Flashcards and Notes: each path with its title and
secondary title, dimmed once completed; Edit, Reset progress (with an
undo) and Delete (with an undo) behind its options. The studyset opens
on its paths while one is unfinished, on its flashcards otherwise.
- "Create a path" in Add material asks for the titles in a sheet and
opens the new path to write; "Import a path (JSON)" reads a file in
the import format, says what came in and how many blocks were left out
(images and animations: the app never fetches from the web).
- The player: one step at a time at reading width, a bar of the steps
above, Back and "Check answers" / "Continue" / "Finish" below. It
opens at the first unfinished step; an exercise passes only once
answered right, as often as it takes; after a check, only the
learner's own picks are marked, with the author's feedback; sources
behind a chip; a finish screen leading to the next path. Words and
items are dragged into place, or tapped into place for phones.
- Writing: the path's screen (titles, description, tags, steps added by
kind, reordered, deleted with an undo) and the step's (title, text
with the format bar, its kind's exercise, feedback for right and
wrong in the whole window, sources). The step saves on what it
started from, as notes do, read-only while devices disagree until a
version is kept.
- The library and the topics count paths; steps done count on the
dashboard, for the streak and the activity.
- The rich-text screen for a studyset's description moves to core, for
the steps' feedback too, and gains the format bar.
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>
The sync engine (serverpod_offline_sync v0.0.8) keeps rebuilding the
projections of every space on every session of a server whose first
reconciliation changed its registry of synced tables
(serverpod_offline_sync#150, open). `serverpod start` migrates in the
running server, so after such a migration the development server is
slow until restarted; deploys reconcile in their maintenance run and
aren't affected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
`flutter pub get --enforce-lockfile` installs what pubspec.lock says or
fails with pub's own message, so CI never resolves something else on
its own.
The second try works around https://github.com/dart-lang/pub/issues/4912
(Dart v3.13): when the runner's cached copy of a git dependency's
repository lacks the locked commit, pub resolves that package without
its dependencies. That failed the CI run of 079179d with Flutter's
"package_graph.json: dependencies for `universal_platform` missing".
With --enforce-lockfile the first run fails at pub instead, having
fetched the commit, and the second goes through; checked locally on a
copy of the pub cache with the editor fork's commit removed. pub's fix
(ebba0dc) is in no Dart release yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
- A Notes tab beside Flashcards lists a studyset's notes in order: the
title ("Untitled note" without one) and when each was last edited
("Edited 5 minutes ago", by the calendar from yesterday on, the date
after a week); Edit and Delete behind its options. Deleting asks,
then offers Undo for a moment (`NoteDeletions`). With none, the tab
says so.
- "Create a note" in Add material opens a new note over the tabs; so
does a note of the list. The note: a title, its tags once it exists,
and its text at reading width; the app bar says how saving stands.
- `NoteEditorBloc` saves after 5 seconds without typing, when the app
goes to the background, and when the note is closed (the bloc saves
as it closes, so leaving never waits, and the write outlives the
screen). Each save builds on the revisions the editor started from;
another device's change takes over a note with nothing unsaved, and
merges with one that has. A note changed differently on two devices
is read-only until the person keeps a version of each field in
conflict. A new note is created by its first save, never when left
blank; a note deleted elsewhere says it no longer exists.
- The editor gains a format bar, docked below it while it has the
focus (`RichContentEditor(formatBar: true)`): alignment, bold,
italic, underline, superscript and subscript. It takes no focus, so
the selection stays. Superscript and subscript are inline marks of
their own, drawn by the font's raised and lowered glyphs in editors
and read-only text alike.
- The library's and the topics' counts include notes (a studyset with
what is inside it, as for cards).
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>
`mergeRevisionsOf` merges the revisions of any `MergeableContent` field
by field against their newest common ancestor, as the card merge did;
cards keep `mergeRevisions`, now a wrapper naming their fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The composer saved a stored card with the card's current heads as its
parents, so whatever had arrived meanwhile was replaced, not merged:
- another device's change to the side the person left alone was
overwritten silently;
- an edit made while saving, saved before the card was read back,
stood beside the first save: a conflict with itself;
- a new card left again before it was read back wasn't written at
all, while it showed as saved.
A draft now keeps the revisions its content came from (`base`): taken
with the stored content while the person hasn't changed it, and its own
revision after each save. A save replaces exactly those
(`CardRepository.saveEdit`), so what another device wrote meanwhile
stays a head of its own and merges per side (ADR 0003): a side only it
changed comes through, a side both changed is a conflict to settle. A
changed draft takes new conflicts only once it is saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The flow ran on a Pixel 9a and its emulator (2026-10-02); what it found:
- The composer stayed on the card just written after "New flashcard", and
on the original after "Duplicate": leaving a card changed the state
before the new one was in it, so the screen took the new card for gone.
It now waits for the card it asked for.
- Bottom sheets reached under the status bar with the keyboard open
(useSafeArea).
- The field being written stayed behind the keyboard: the editor scrolls
only within itself, so the card face now keeps it in view when it opens
and when the keyboard rises, as a text field does.
- A tap on read-only rich text put a caret in it and opened the keyboard on
phones, and in the composer never opened the field. RichContentView is
display only now: no selection, taps go to what surrounds it, a sound's
button still plays (decided 2026-10-02).
- The card types broke their names on a phone; they are choice chips that
wrap (decided 2026-10-02).
- The week overview marked days before the person's first activity as
missed; they show nothing now. An import starts the activity too, without
counting as studying (decided 2026-10-02).
- A composer preview cut short gave no sign of it; it fades out over its
last 32 dp, only when cut short (decided 2026-10-02).
- "Isn't on this device yet" showed for any image or sound that failed;
a file that is here but can't be decoded now says so, and offers no retry.
- The signed-in account card was narrower than the anonymous one.
- The composer's title said "Create flashcards" also when opened on a
stored card; it says "Edit flashcards in …" then (decided 2026-10-02).
- The Anki fixtures carry a valid PNG and a silent MP3, so they also serve
to try the import by hand (decided 2026-10-02); regenerated with Anki
26.9.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The editor's fork moves to keyboard_height_plugin v0.3.0 (83bdb2a3): v0.1.5
compiled against API 31, while the AndroidX libraries of Flutter v3.47 need
34, so Gradle's AAR metadata check failed. Release builds get the internet
permission, which Flutter's template grants to debug and profile builds
only. Gradle's own build folder in android/ (its problems report) is
ignored. AGENTS.md: the server's test/integration/test_tools/ is generated
too, so test files are formatted one by one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
An anonymous person may keep 100 MB of images and sounds, an identified one
500 MB, and one person more by a file_allowance row the operator adds; all
of them together 15 GB, since every file is in the database and in each dump
the server keeps (decided 2026-10-02). Each comes from the environment
(FILE_LIMIT_*_MB), with these defaults, documented in deploy/.env.example.
The server checks when an upload starts, counting unfinished uploads so they
can't fill the disk unnoticed, and again when it finishes, counting what is
kept, so uploads started side by side can't overshoot; a refusal is a
FileLimitExceededException, and an upload refused as it finishes is deleted.
deletePerson deletes the allowance with the person. Migration: the new
table file_allowance, only added.
The app keeps a refused file waiting on the device and stops that round of
uploads; the Anki import warns when a deck's files may not fit in the room
left (asset.usage()).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
They are the way back from a deploy, not backups: those go off-site (3c).
Every file people keep is in each dump, so fewer dumps leave room for more
files (decided 2026-10-02).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
AGENTS.md: the file endpoints and the app's files, asset URLs in content,
imported cards, platform plugins behind interfaces, server tests on the sync
engine's database, and GStreamer for Linux builds. ADR 0004: content names
files as asset:<id> URLs, and how stored files are deleted.
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>
The asset endpoint (login required) takes a file into the caller's own space
by its SHA-256 and serves an asset row's file only to its space's owner and
members; anyone else, and a deleted row, gets NotFoundException as for a
missing file. startUpload hands out Serverpod's upload description, or null
when the file is there already (claiming it anew); finishUpload keeps the
upload only if its content has the announced hash. Files are stored in the
private database storage at spaces/<space>/<sha256>, at most 20 MB.
deletePerson deletes the person's files in its transaction, and a daily
future call deletes the files no live asset row refers to, and uploads never
finished, 7 days after they were last claimed (ADR 0004, rule 5).
The tests run on the sync engine's database, as the server does, without
rollback: its writes take transactions of their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The home tab stays open below a study session, and every rating changed
what it shows; it now waits until changes pause for 300 ms, so a large
library isn't read anew after each rating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Home opens on a study dashboard. The streak's flame sits in the app bar
and opens a sheet of how it stands this week. Below: the study goal (a
ring of the day's or week's progress, set and removed in its sheet), the
week (each day studied, rested, missed or to come with the cards falling
due; days to go back and on; today or a day to come made a rest day),
and a year of activity as a heatmap, its weekdays staying in view while
the weeks scroll. A long press arranges the dashboard: drag a widget
elsewhere, hide or show it; the arrangement is synced.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
Against the running server: a word's reading changed on one device and
its meanings on another both survive; a review back to front syncs with
its mode and moves only that schedule, and a fresh start syncs too.
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 studyset's options offer Reset progress: after asking, every card
in it and in the studysets inside it starts afresh as new (a 'reset'
entry each), its history kept; an undo for a few seconds deletes those
entries again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A session studies a round of up to the settings' cards per round. In
spaced repetition a card leaves the round once it is a day or more away,
one rated lower comes back when due; cramming walks every card once,
round after round, its reviews moving no schedule, with going back and
on. Asking the back first (a toggle in the header, saved in the
settings) asks front/back and vocabulary cards their back, on their own
schedule. Each type studies its way: a word shows its reading and
meanings, a multiple-choice card is picked and checked, a matching card
sorted by dragging or tapping and checked; then the person rates (Bad,
OK, Good, Perfect, each with when the card comes back). Progress shows
as pills, or a tally bar while cramming; a round ends with how it went.
The session settings gain the mode, cards per round, new cards per day
and the order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
How the person studies, the same on every device: spaced repetition or
cramming, the order (newest first for cramming only), cards per round
(5 to 20), new cards a studyset starts a day (0 for no limit), and
whether cards ask their back first. The settings payload's schema 2
holds them, each left out while as it comes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Each review records its mode (ADR 0004, rule 3): front to back, back to
front with a schedule of its own, or crammed outside the schedule. A
card's schedule replays only the reviews of one mode. Resetting progress
appends a 'reset' entry per card, no migration (the column is text):
schedules replay what follows it, and the history stays for the streak
and the heatmap. Reviews are inserted in batches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset's cards list as bordered rows of one height showing the
front, the card's tags above it and a badge when devices changed it
differently; tapping opens the composer, "…" offers editing and
deleting (after asking).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The editor and its read-only view used the editor's own defaults: black
text, unreadable in dark mode, and 100 dp of padding beside it on
desktops. Both now take the theme's text, link and selection colours and
leave the padding to what holds them; phones get the touch variant with
selection handles. A multiple-choice card's last right answer keeps its
switch enabled (and its colours); turning it wrong does nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Create flashcards, and tapping a card, open the composer: the stack of
cards on the left (New flashcard on top, the new ones, then the stored
ones; collapsible), the card being written on the right, saved as the
person leaves it (another card, the pane, the screen, the app). The
title says how saving stands; leaving with cards that couldn't be saved
asks first. A card has its tags and their pin, an action menu
(duplicate, clear, delete), the type switch (flashcard, vocabulary,
matching), the front, and below a line its answer side: one answer or
several to mark right or wrong, the reading and meanings, or categories
and items. Fields changed differently on two devices show every version
to keep one. The single-card editor goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A bloc holding a studyset's cards as drafts, new ones on top and the
stored ones below. A card is saved when the person leaves it, once it
changed and has everything its type asks for, only its type's fields
stored; a failed save waits for the next leaving. A new card takes the
type of the card before it and what is pinned of it (front, answer,
tags); duplicating, clearing and deleting; a card devices changed
differently first needs a version chosen for every field in conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A card has a type: front/back, multiple choice (drawn on a map when it
names one), vocabulary or matching, and the fields of that type. The card
payload's schema 2 holds the type and the fields in use; schema 1 reads
as front/back. Revisions merge per field rather than per side (ADR 0003,
amended): a field one device changed takes the change, a field changed
differently is a conflict, lists included as a whole.
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>
The card editor offers its tags: a picker that searches by the start of
a name, creates a tag (name and colour) and chooses it, and saves the
choice with the card. A sheet lists every tag with its count, to add,
rename (merging into a tag of that name), recolour and delete them, with
an undo. The card list shows each card's tags and narrows to one tag.
The editor closed twice once tags changed after saving; it now closes
on becoming done only.
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>
A studyset's screen, as decided with the tree (2026-09-30/10-01):
- A breadcrumb from the library down to the studyset; its steps go up
(replacing the location, so back leads further up), and steps that
don't fit fold into a "…" listing them, nearest first.
- From expanded windows on, two columns: the description over the topics,
the flashcards beside them, Study and Add material in the app bar. Below
that, the topics are in a drawer, opened from a bar at the bottom that
also holds Study and Add material and slides away while reading onwards;
the back button stays where a drawer would have taken its place.
- The topics: "All topics" for the whole studyset, then the studysets
inside it in their order, each with the study it invites, what it holds
and its options (edit, delete), opening on top of this screen; a switch
rearranges them by dragging; "New sub-studyset" creates one inside.
- Study covers the studyset with everything inside it ("Learn 12" while
something is due). Add material offers flashcards for now.
- Options: Edit (the sheet, now with the description, edited in the whole
window, and for a studyset inside another the colour of the one it is
in, which a new one takes at first), Move to… (the tree and the top
level, without the studyset itself and what is inside it), Archive or
Unarchive with Undo, Delete with Undo, which closes the screen.
- The description cut short when long, the whole a tap away; the licence
as the last line, linking to its text where that is known.
The list's bloc, scoped to a parent, lists its children, works out the
studyset's own progress, creates inside it, reorders and moves. The card
list leaves adding and studying to the screen. The inline editing of the
title and description, and its bloc, are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Studying a studyset covers its subtree (decided with the tree, ADR 0004):
the studysets of its subtree in tree order, without archived studysets and
what is inside them (studiedIn); an archived studyset studies nothing. The
study queue takes the cards of each studyset as a group: due cards of every
studyset first, earliest first, then each studyset's new cards in tree
order, at most 20 started per studyset and local day, as before for one.
The library's cards count the same way, so a studyset whose cards sit in
its sub-studysets still invites a session, and the badge counts what the
session would study; an archived studyset, paused, still counts all it
holds.
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>
The model learns the tree (ADR 0004): a studyset's children in order (by
position, ties by id), its subtree in tree order (the studyset, then each
child with its own subtree), and its ancestors from the top level down,
for the breadcrumb. Studysets moved into each other on two devices make a
cycle; subtrees stop where it comes back, and climbing stops at the
studysets that show at the top level.
The repository creates a studyset inside a parent, after its siblings;
moves one into another studyset or to the top level, after the studysets
already there, writing only its parent, position and updatedAt; refuses a
move into the studyset itself or anything inside it; and reorders a
studyset among its siblings with a fractional index, as cards do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01. CI run #32 died in its analysis: a cold `dart analyze`
of the workspace with the lint plugin now peaks at about 3.7 GB (2.1 GB
warm), measured with an empty analysis cache, while the runner's unit
allowed 3 GB. The kernel killed it, systemd's default OOMPolicy (stop) then
stopped the whole unit, and the job's remaining processes got SIGTERM: the
"signal: terminated" in the log. The packages this branch added tipped it
over.
The runner may now use 5 GB of sahaquiel's 8, and above 4 GB the kernel
reclaims from CI first (MemoryHigh), so the instance keeps at least 3 GB;
it uses under 1 GB today. Installed by hand on sahaquiel, like the rest of
the unit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Since the app generates its localizations (`generate: true`), `flutter pub
get` at the workspace root, as CI runs it, writes build/.last_build_id
there. CI's generated-code check counts that untracked file as a difference
and failed run #31 after 24 seconds; a developer running pub get inside the
app package never sees it, because the app ignores its own build/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01. Deleting still asks first; once confirmed, the studyset
leaves the library at once and "Studyset deleted" offers Undo for three
seconds. Only when the time to undo is over (four seconds: the message's
three and a moment for a late tap) are its cards and then the studyset
blanked and deleted, since blanking can't be undone (ADR 0002, rule 2). A
deletion still waiting when the library closes (the app quits, the person
changes) is dropped, and the studyset stays.
The library's bloc keeps the waiting deletions; the event that ends the
time to undo is private, so no screen can skip it. AGENTS.md now describes
the library's shared widgets, the sheets (and why a sheet that asks before
discarding can't be swiped away), the order of studysets, and this Undo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01: after the starred ones, the library lists the most
recently changed studysets first. That needs a change date, so studyset
gains updatedAt (ADR 0004): when the studyset itself last changed, its
content, place, star or archive date, written by the device making the
change in the same update as the change; the last writer wins. Studysets
from before have none and come last, the newest of them first.
The branch's migration is still unreleased (not merged, not deployed), so it
is recreated rather than followed by a second one:
20261001064926911-tree-tags-assets replaces 20260930213716558 and differs
only by one line on the server, ALTER TABLE "studyset" ADD COLUMN
"updatedAt" timestamp without time zone (nullable, so existing rows keep
null and no data changes), the same column in the table devices rebuild,
and its version. The model's comment on the payload no longer lists schema
1's fields alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Decided 2026-10-01, in place of "closing the account dialog cancels it",
which had no dialog left once the account moved into the Profile tab:
choosing another tab cancels a sign-in waiting for GitHub in the browser,
since the person has left it behind. Settings, opened over the profile, is
not leaving it.
Screens learn whether their tab shows from TabVisibility, which the tabs'
container now provides: go_router's indexed stack as before (every tab
alive, hidden ones offstage with their animations paused), each tab wrapped
in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The library tab becomes a grid of studyset cards (up to 400 dp wide, all
equally tall), starred ones first, and the archive as a section of its own
below, closed until opened and counting what rests there. It lists the top
level: studysets without a parent, those whose parent is gone, and those in
a cycle (ADR 0004). Each card's progress follows the studysets and every
change of a card, here or synced, and is worked out again after a study
session; a slow, older working out never overwrites a newer one.
- Search: a field below the app bar filters by title and secondary title,
opens the archive while it finds something there, and says when nothing
matches; closing it shows everything again.
- Creating and editing: a sheet with the title, the secondary title (typed
in capitals, as every surface shows it) and the colour, picked from the
palette's twenty, whose names are localised; a new studyset is cobalt,
and an existing one keeps its colour, even one only a newer app version
knows, until another is picked. Create waits for a title. Closing the
sheet with changes asks first. It can't be swiped away: a bottom sheet
swiped away closes without asking its PopScope (Flutter issue 152120,
still open), so such sheets have no drag handle and close by their close
button, the back gesture or a tap beside them, which all ask.
- Archiving doesn't ask, since nothing is lost; it offers to undo, as does
unarchiving. Deleting asks, then deletes the studyset with its cards, each
blanked first.
- The create button floats in compact windows and slides away while
reading onwards; from medium windows on it sits in the app bar.
The old list and its title-only dialog are gone; the studyset screen's
tests create studysets through the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset in the library: a bar in its colour, its title with the secondary
title as a kicker, what it holds, and beside the title the action it
invites inside a ring its mature cards fill, with a badge counting the due
cards (at most "99+", growing inwards so it never passes the card's edge).
The action is a review when reviews are due, studying when only new cards
are, and adding content otherwise, which opens the studyset: the card only
invites a session that has something to study. A long press stars it, as
does the star, which a hovering pointer reveals and which stays while
starred; the options edit, archive or unarchive, and delete it. An archived
studyset rests: dimmed, neutral, paused. Until its progress is known, a
muted bar stands in for "No content yet", which it might not be.
A starred star is amber, dark enough in the light theme to stand out from
the cards as a graphic must (3:1), a colour the theme extras now hold. It is
Material's filled star: Lucide has no filled one, and the fill is what says
the star is on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
What a library card shows about its studyset's cards: how many there are,
how many a session started now would study (the study queue's length, so the
card only invites a session that has something to study), how many of those
are reviews rather than new cards, and how many are mature.
A card is mature once FSRS's stability, the days it is remembered with a 90 %
chance, reaches 21: the three weeks after which spaced-repetition apps
traditionally call a card mature, measured by memory rather than by the
interval. A schedule now carries its stability.
Replaying every card's schedule from its reviews moves from the study
session's bloc into the review repository (schedulesOf), and the start of the
learner's local day into a function beside the study queue, so the session
and the library compute alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A studyset is now its content (StudysetContent, everything the synced payload
holds) and the columns that change on their own: where it sits (parent,
position), and when it was archived or starred. Like a card's content, the
payload's part is edited as one value; the columns are written one by one,
so starring or archiving on one device and editing on another all survive a
sync (ADR 0004).
The payload's schema 2 adds the secondary title, the colour (a palette name),
the licence (SPDX identifier or name, holder, year) and the languages of the
cards' two sides (BCP 47), each left out when not given. Schema 1 reads as
schema 2 without them, and a field of the wrong kind makes the payload
unreadable rather than half read. Older app versions skip schema 2 studysets,
which costs nothing before the first release; fields foreseeable for the
first release's features are in, so none of them needs a schema 3 later.
The repository stars, unstars, archives and unarchives (at clock time, in
UTC), writing only that column. A new sync test (run against a server)
checks that a star and an archive on one device and a rename on another all
arrive on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The pieces the library's cards and its create/edit sheet are built from, each
with its tests:
- TitleWithKicker: a title with an optional kicker above it (a studyset's
secondary title, uppercased in the primary colour), a trailing action lined
up with the title, and a reserved kicker line so cards side by side line up.
- ContentCountsLine: "3 studysets • 52 flashcards", leaving out what is
unknown or zero, with a placeholder when nothing is left; labels are plural
messages, so every language words its own counts.
- showAdaptiveSheet and SheetScaffold: a bottom sheet below expanded windows
and a dialog from there on (a bottom sheet is a touch surface; expanded
windows are mostly desktops). The title with its close button and the
footer stay in view, only the body scrolls; the close button asks the
route to close, so a sheet can ask before discarding changes.
- showUndoSnackBar: what just happened, with Undo for three seconds.
- ScrollHidingFab: a floating action button that slides away while reading
onwards and returns on scrolling up, and stays where nothing scrolls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app gets its look and navigation before more features land on them.
- Theme (core/theme/): hand-written light and dark Material 3 colour schemes around
cobalt and copper; ThemeExtras for the colours Material has no role for (states, the
four FSRS ratings); the 20 colours a person can give content. Tests hold text to WCAG
AA contrast on each palette colour and on the dark theme's primary colour and surface.
- Layout and shared widgets: Material 3's window-size classes (core/layout/);
SectionCard, AppLoadingIndicator, confirmDestructiveAction and FoundById
(core/widget/). Lucide icons.
- Navigation with go_router: four tabs (Home, Library, Explore, Profile), each keeping
its own stack of screens, as a navigation rail with the app's icon from 600 dp and as
a navigation bar below; choosing the open tab again returns to its first screen. A
studyset's screen sits in the library; studying, cards and settings cover the tabs.
- Routes carry ids: FoundById finds the item in the repository's live list, so a
studyset deleted on another device, or an old link, says it no longer exists. A
screen that deletes its own item keeps showing it while it closes, rather than
flashing that message.
- Every person gets an app of their own (PersonApp, lib/src/app/): a
MaterialApp.router with its own GoRouter and PersonScope around the navigator, built
by SessionGate inside the person's keyed subtree. Screens read the person's
repositories wherever they are built, so the static route() helpers and the listener
that closed every pushed screen on a change of person are gone: a change of person
now disposes the whole router and starts again at home.
- The account dialog becomes the Profile tab, with Settings (sign out, delete the
account, About) behind a gear. A waiting GitHub sign-in now keeps waiting while
another tab shows; a change of person still cancels it.
- The new screens' strings come from Flutter's gen-l10n (English for now). The
generated files are committed, left out of analysis and formatting, and written
anew by `flutter pub get`, so CI's check of generated code also catches stale ones.
Widget tests now start the whole app (test/support/pump_app.dart) at the screen's path
and move through it as a person would, by tabs and taps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>