halkyon/halkyon_learn_flutter/test/support/test_dependencies.dart

368 lines
16 KiB
Dart
Raw Normal View History

Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
import 'dart:io';
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_file_cache.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_gateway.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_server_gateway.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/sound_player.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/data/device_settings_store.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
import 'package:halkyon_learn_flutter/src/core/files/file_picker.dart';
import 'package:halkyon_learn_flutter/src/core/files/file_saver.dart';
import 'package:halkyon_learn_flutter/src/core/sync/pending_changes_store.dart';
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
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 11:31:37 +02:00
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
feat(app): editor blocks: callout, spoiler, formulas, files, video, study button 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>
2026-10-03 12:20:41 +02:00
import 'package:halkyon_learn_flutter/src/core/video/youtube_videos.dart';
import 'package:halkyon_learn_flutter/src/feature/about/data/about_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/about/data/about_repository.dart';
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package.dart';
import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_package_reader.dart';
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:39:07 +02:00
import 'package:halkyon_learn_flutter/src/feature/archive/data/archive_reader.dart';
import 'package:halkyon_learn_flutter/src/feature/archive/model/import_archive.dart';
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 12:13:00 +02:00
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/data/claude_hand_off.dart';
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 14:41:36 +02:00
import 'package:halkyon_learn_flutter/src/feature/assistant/data/web_search.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/copy_updater.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.dart';
import 'package:halkyon_learn_flutter/src/feature/export/data/export_archive.dart';
import 'package:halkyon_learn_flutter/src/feature/export/data/studyset_markdown.dart';
feat(feedback): reports and suggestions on published studysets, gathered by the instance 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>
2026-10-10 01:04:17 +02:00
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
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 11:31:37 +02:00
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_store.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_suggestion.dart';
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.dart';
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_answer_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
import 'package:halkyon_learn_flutter/src/feature/note/data/note_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
feat(app): a Pomodoro timer on Home, in the notification and in the top bar 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>
2026-10-07 02:54:52 +02:00
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_platform.dart';
import 'package:halkyon_learn_flutter/src/feature/pomodoro/data/pomodoro_run_store.dart';
feat: public profiles, a handle on the account domain with name, bio, links and picture 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>
2026-10-09 20:55:16 +02:00
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/publish/data/published_store.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
feat(app): study with FSRS 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>
2026-09-30 16:59:07 +02:00
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
import 'fake_about_gateway.dart';
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
import 'fake_asset_gateway.dart';
import 'fake_asset_server_gateway.dart';
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 12:13:00 +02:00
import 'fake_assistant_gateway.dart';
import 'fake_auth_gateway.dart';
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
import 'fake_card_gateway.dart';
import 'fake_dictionary_gateway.dart';
import 'fake_explore_gateway.dart';
feat(feedback): reports and suggestions on published studysets, gathered by the instance 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>
2026-10-10 01:04:17 +02:00
import 'fake_feedback_gateways.dart';
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
import 'fake_file_picker.dart';
import 'fake_file_saver.dart';
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 11:31:37 +02:00
import 'fake_jellyfin.dart';
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
import 'fake_learning_path_gateway.dart';
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
import 'fake_note_gateway.dart';
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
import 'fake_path_answer_gateway.dart';
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
import 'fake_pdf_engine.dart';
import 'fake_pdf_source_gateway.dart';
feat(app): a Pomodoro timer on Home, in the notification and in the top bar 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>
2026-10-07 02:54:52 +02:00
import 'fake_pomodoro_platform.dart';
feat: public profiles, a handle on the account domain with name, bio, links and picture 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>
2026-10-09 20:55:16 +02:00
import 'fake_profile_gateway.dart';
import 'fake_publish_gateway.dart';
feat(app): study with FSRS 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>
2026-09-30 16:59:07 +02:00
import 'fake_review_gateway.dart';
import 'fake_settings_gateway.dart';
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
import 'fake_sound_player.dart';
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
import 'fake_studyset_gateway.dart';
import 'fake_tag_gateway.dart';
feat(app): editor blocks: callout, spoiler, formulas, files, video, study button 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>
2026-10-03 12:20:41 +02:00
import 'fake_youtube_videos.dart';
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
import 'memory_asset_file_cache.dart';
import 'memory_device_settings_store.dart';
feat(app): a Pomodoro timer on Home, in the notification and in the top bar 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>
2026-10-07 02:54:52 +02:00
import 'memory_pomodoro_run_store.dart';
import 'memory_published_store.dart';
import 'memory_studyset_tab_store.dart';
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
/// The app's dependencies with fakes instead of the server and the device's
/// database; each test passes in only the fakes it looks at.
///
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
/// Every person gets [studysetGateway], [cardGateway], [noteGateway],
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
/// [pathGateway], [answerGateway], [tagGateway], [settingsGateway] and the
/// asset fakes (or fresh fakes of their own),
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
/// unless [openPerson] replaces how a person's dependencies are opened.
///
/// The device starts with [deviceSettings], kept in memory unless a test
/// passes the [deviceSettingsStore] to look at.
///
/// A person's sync runs [syncOnce] (by default, going through without doing
/// anything): once when their data opens if a test passes it, and whenever
/// the person asks; never on timers, which a widget test would find pending.
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
Dependencies testDependencies({
AuthGateway? authGateway,
StudysetGateway? studysetGateway,
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
CardGateway? cardGateway,
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
NoteGateway? noteGateway,
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
LearningPathGateway? pathGateway,
PathAnswerGateway? answerGateway,
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
PdfSourceGateway? pdfGateway,
feat(app): study with FSRS 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>
2026-09-30 16:59:07 +02:00
ReviewGateway? reviewGateway,
TagGateway? tagGateway,
GeoMapRepository? maps,
StrokeRepository? strokes,
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
SoundPlayer? sounds,
FilePicker? files,
FileSaver? fileSaver,
feat(app): editor blocks: callout, spoiler, formulas, files, video, study button 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>
2026-10-03 12:20:41 +02:00
YoutubeVideos? videos,
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
PdfEngine? pdfEngine,
SettingsGateway? settingsGateway,
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
AssetGateway? assetGateway,
AssetServerGateway? assetServer,
AssetFileCache? assetFiles,
AboutGateway? aboutGateway,
DictionaryGateway? dictionaryGateway,
DeviceSettingsStore? deviceSettingsStore,
DeviceSettings deviceSettings = const DeviceSettings(),
AppVersion appVersion = const AppVersion(version: '1.0.0', build: '1'),
feat(app): a Pomodoro timer on Home, in the notification and in the top bar 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>
2026-10-07 02:54:52 +02:00
PomodoroPlatform? pomodoro,
PomodoroRunStore? pomodoroRuns,
feat: public profiles, a handle on the account domain with name, bio, links and picture 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>
2026-10-09 20:55:16 +02:00
ProfileGateway? profileGateway,
PublishGateway? publishGateway,
PublishedStore? publishedStore,
StudysetTabStore? studysetTabStore,
ExploreGateway? exploreGateway,
feat(feedback): reports and suggestions on published studysets, gathered by the instance 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>
2026-10-10 01:04:17 +02:00
FeedbackWriteGateway? feedbackWriteGateway,
FeedbackServerGateway? feedbackServerGateway,
SyncOnce? syncOnce,
PendingChangesStore? pendingChanges,
OpenPerson? openPerson,
ClaudeHandOff? claude,
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 14:41:36 +02:00
WebSearch? webSearch,
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 12:13:00 +02:00
AssistantGateway? assistantGateway,
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 11:31:37 +02:00
JellyfinGateway? jellyfinGateway,
JellyfinStore? jellyfinStore,
JellyfinSuggestion? jellyfinSuggestion,
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 11:31:37 +02:00
MediaPlayers? mediaPlayers,
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
}) => Dependencies(
serverName: 'learn.example.org',
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
auth: AuthRepository(authGateway ?? FakeAuthGateway()),
about: AboutRepository(aboutGateway ?? FakeAboutGateway()),
dictionaries: DictionaryRepository(
dictionaryGateway ?? FakeDictionaryGateway(),
),
maps: maps ?? GeoMapRepository(),
strokes: strokes ?? StrokeRepository(),
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
sounds: sounds ?? FakeSoundPlayer(),
files: files ?? FakeFilePicker(),
fileSaver: fileSaver ?? FakeFileSaver(),
feat(app): editor blocks: callout, spoiler, formulas, files, video, study button 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>
2026-10-03 12:20:41 +02:00
videos: videos ?? FakeYoutubeVideos(),
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
pdfEngine: pdfEngine ?? FakePdfEngine(),
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
ankiPackages: InlineAnkiPackageReader(),
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:39:07 +02:00
archives: InlineArchiveReader(),
exportArchiver: InlineExportArchiver(),
deviceSettingsStore:
deviceSettingsStore ?? MemoryDeviceSettingsStore(deviceSettings),
deviceSettings: deviceSettings,
appVersion: appVersion,
feat(app): a Pomodoro timer on Home, in the notification and in the top bar 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>
2026-10-07 02:54:52 +02:00
pomodoro: pomodoro ?? FakePomodoroPlatform(),
claude: claude ?? ClaudeHandOff(open: (url) async => true),
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 14:41:36 +02:00
webSearch:
webSearch ??
WebSearch(
open: (url) async => true,
deviceSearch: (query) async => false,
),
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 12:13:00 +02:00
assistant: AssistantRepository(assistantGateway ?? FakeAssistantGateway()),
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 11:31:37 +02:00
jellyfin: JellyfinRepository(
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
jellyfinStore ?? MemoryJellyfinStore(),
deviceName: 'Test',
appVersion: appVersion.version,
),
jellyfinSuggestion:
jellyfinSuggestion ?? JellyfinSuggestion(() async => null),
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 11:31:37 +02:00
mediaPlayers: mediaPlayers ?? FakeMediaPlayers(),
openPerson:
openPerson ??
(person) async {
final sync = SyncService(
syncOnce ?? ({required onMerge}) async {},
pendingChanges: pendingChanges,
);
if (syncOnce != null) {
sync.syncNow();
}
final studysets = StudysetRepository(
studysetGateway ?? FakeStudysetGateway(),
);
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
final cards = CardRepository(cardGateway ?? FakeCardGateway());
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
final notes = NoteRepository(noteGateway ?? FakeNoteGateway());
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
final paths = LearningPathRepository(
pathGateway ?? FakeLearningPathGateway(),
);
final pathProgress = PathProgressRepository(
answerGateway ?? FakePathAnswerGateway(),
);
final tags = TagRepository(
tagGateway ?? FakeTagGateway(),
cardChanges: cards.changes,
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
noteChanges: notes.changes,
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
pathChanges: paths.changes,
);
final settings = SettingsRepository(
settingsGateway ?? FakeSettingsGateway(),
);
final reviews = ReviewRepository(reviewGateway ?? FakeReviewGateway());
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
final assets = AssetRepository(
assetGateway ?? FakeAssetGateway(),
assetServer ?? FakeAssetServerGateway(),
assetFiles ?? MemoryAssetFileCache(),
);
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
final pdfSources = PdfSourceRepository(
pdfGateway ?? FakePdfSourceGateway(),
assets,
);
final deletions = StudysetDeletions(
studysets,
cards,
notes,
paths,
pdfSources,
);
final noteDeletions = NoteDeletions(notes);
final cardDeletions = CardDeletions(cards);
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
final pathDeletions = LearningPathDeletions(paths);
final stepDeletions = StepDeletions(paths);
final pdfDeletions = PdfDeletions(pdfSources);
final studysetTabs = StudysetTabs(
studysetTabStore ?? MemoryStudysetTabStore(),
);
await studysetTabs.open();
return PersonDependencies(
sync: sync,
studysets: studysets,
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
cards: cards,
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
notes: notes,
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
paths: paths,
pathProgress: pathProgress,
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
pdfSources: pdfSources,
reviews: reviews,
tags: tags,
settings: settings,
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
assets: assets,
deletions: deletions,
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
noteDeletions: noteDeletions,
cardDeletions: cardDeletions,
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
pathDeletions: pathDeletions,
stepDeletions: stepDeletions,
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
pdfDeletions: pdfDeletions,
feat(app): a Pomodoro timer on Home, in the notification and in the top bar 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>
2026-10-07 02:54:52 +02:00
pomodoroRuns: pomodoroRuns ?? MemoryPomodoroRunStore(),
studysetTabs: studysetTabs,
feat: public profiles, a handle on the account domain with name, bio, links and picture 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>
2026-10-09 20:55:16 +02:00
// The picture as it is: drawing it anew is real work a widget
// test's fake clock can't wait for (avatar_image_test.dart has it).
profile: ProfileRepository(
profileGateway ?? FakeProfileGateway(),
drawAvatar: (picture) async => picture,
),
publishing: PublishRepository(
publishGateway ?? FakePublishGateway(),
publishedStore ?? MemoryPublishedStore(),
),
explore: ExploreRepository(
exploreGateway ?? FakeExploreGateway(),
ReleaseCopier(
studysets,
cards,
notes,
paths,
pdfSources,
tags,
assets,
),
CopyUpdater(
studysets,
cards,
notes,
paths,
pdfSources,
tags,
assets,
),
studysets,
),
feat(feedback): reports and suggestions on published studysets, gathered by the instance 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>
2026-10-10 01:04:17 +02:00
feedback: FeedbackRepository(
feedbackWriteGateway ?? FakeFeedbackWriteGateway(),
feedbackServerGateway ?? FakeFeedbackServerGateway(),
cards,
notes,
paths,
assets,
),
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
onDispose: () async {
await deletions.dispose();
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
await noteDeletions.dispose();
await cardDeletions.dispose();
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
await pathDeletions.dispose();
await stepDeletions.dispose();
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
await pdfDeletions.dispose();
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
await studysets.dispose();
await tags.dispose();
await settings.dispose();
await reviews.dispose();
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
await cards.dispose();
feat: notes in studysets, synced and merged as cards are, with tags 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>
2026-10-02 21:53:56 +02:00
await notes.dispose();
feat: learning paths in studysets, synced, with progress and tags 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>
2026-10-02 23:31:20 +02:00
await paths.dispose();
await pathProgress.dispose();
feat: PDF sources: read, take notes and cards from pages, deadlines 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>
2026-10-03 14:40:08 +02:00
await pdfSources.dispose();
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
await assets.dispose();
await sync.dispose();
feat(app): flashcards on append-only revisions 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>
2026-09-30 14:41:44 +02:00
},
);
},
feat(app): studysets, offline first and synced on triggers The first synced feature. Every person gets their own local database (<app support>/people/<authUserId>/sync.sqlite): PersonScope opens it inside SessionGate's per-person subtree through Dependencies.openPerson, together with its sync and the person's repositories (PersonDependencies), and closes it when the person changes. SessionGate then also closes pushed screens, which live above the person's subtree; a screen's route() hands its bloc the repository it needs. SyncStudysetGateway reads and writes the studyset table through the sync session, names the payload column in every update and asks for a sync after every write. StudysetRepository encodes and decodes the payload (studyset_payload.dart: UTF-8 JSON with a schema version; a newer schema or garbage is skipped with a log), re-reads after its own writes and after merges, and blanks the payload before deleting, since the engine keeps deleted rows until the space is deleted (ADR 0002, rule 2). A failed read reaches the list as an error, and the next change reads again. SyncService syncs on triggers: at start, 2 s after the last local write, on resume and every 5 minutes, one sync at a time, and lets a running sync finish before the database closes. Screens: the list, now the home screen in place of the editor playground, creates studysets through a dialog; a studyset's screen edits its title and description, saves on request and asks before deleting. Tests: the payload codec, the repository against a fake gateway (including a failed read and rows from a newer app), the sync triggers under fakeAsync, both blocs' sequences, and the screens, including that cancelling the delete keeps the studyset. The session tests now settle until the person's data is open. AGENTS.md describes the per-person graph, synced data and the envelope as it now is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 10:11:24 +02:00
);
Import Anki decks with their files and history 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>
2026-10-02 04:52:02 +02:00
/// Reads Anki packages where it is asked to: a widget test's fake clock
/// can't wait for another isolate.
class InlineAnkiPackageReader() implements AnkiPackageReader {
@override
Future<AnkiPackage> read(Uint8List bytes) async =>
readAnkiPackage(bytes, temp: Directory.systemTemp);
}
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:39:07 +02:00
/// Reads archives where it is asked to, as [InlineAnkiPackageReader] reads.
class InlineArchiveReader() implements ArchiveReader {
@override
Future<ImportArchive> read(Uint8List bytes) async => readArchive(bytes);
}
/// Packs exports where it is asked to, as [InlineAnkiPackageReader] reads.
class InlineExportArchiver() implements ExportArchiver {
@override
Future<Uint8List> zip(List<ExportFile> files) async => zipExport(files);
}