Commit graph

1,087 commits

Author SHA1 Message Date
89a3784df4
chore: the website is halkyon.app/learn
Halkyon's website is one site for all its apps, as Proton's is, so
Halkyon Learn's page is halkyon.app/learn rather than a site of its own
at learn.halkyon.app, which redirects there. The snap's and the F-Droid
repository's listings point to it.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:44 +02:00
1c1a1aada8
feat(feedback): report, suggest, and an inbox in the Profile tab
The app's side of stage 4 (ADR 0005, rule 11). The card's Report sheet
offers "Send to the author too" on a card from a release; a copy's menu
reports the studyset as a whole and lists what is reported and
suggested on it, as Explore's summary does; the editors of cards, notes
and steps suggest them to the author as the learner has them, never
under "no derivatives". The Profile tab holds the inbox: the threads on
the person's published studysets and those they wrote, the newest
activity first, with a count on the tab. A thread shows a suggestion
beside the author's own item and its replies; its writer and the author
reply, and the author accepts (the content becomes their own edit, its
files fetched from the writer's), declines, closes, hides or blocks.
Writing needs an identity; reading what is public doesn't.

ADR 0005 gets an addendum for stage 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:43 +02:00
9accc95df2
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 09:46:42 +02:00
7574e42415
feat(explore): how many learn it here, opt-in; copies published within their licence
Explore says how many people learn a published studyset on this instance
(ADR 0005, rule 8): shown, never ranked, counted daily by the server from
the people with an identity who chose to be counted (Settings, Privacy;
settings schema 8) and studied a copy on 3 or more days of the last 30,
rounded down from 5 on, so that nobody can be told apart. Only a number
the instance computes itself counts; votes count for nothing. The author
sees the same rounded number in the Publish sheet.

A copy of a published studyset is published within the original's
licence: never under "no derivatives", under a licence that keeps
adaptations under it only under that one, in every studyset of the copy.
Its releases say what they are based on, and Explore shows it.

ADR 0005 gets an addendum with stage 3's decisions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:41 +02:00
5613530f82
feat(explore): take a later release into a copy, at a tap
A copy's top studyset says when a newer release of what it came from is
out, and what it would bring: changed, new and withdrawn items, counted
against the release the copy last took. Update takes it, never by
itself (ADR 0005, rule 4): cards, notes and steps take the release as a
revision on the one where they last took a release and merge per side,
as a dictionary's corrections do, so what the learner changed stays and
a side both changed is a conflict to settle; studysets, paths and PDFs
take each field the learner left as it was. New items come in where
they belong, unless the learner deleted that part; what the author
withdrew stays, marked, and the card browser filters for it; what the
learner deleted stays deleted; progress is never touched.

The instance gives out any release by its version, for the app to
compare the two. UpdateTaken moves into the shared revision merge, now
that notes and steps take updates too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:41 +02:00
f4af6cfd3a
feat(explore): the Explore tab, and adding a published studyset as a copy
Explore lists what is published on the instance as the old app's feed:
search over titles and authors, filters for languages and kinds of
content, the newest release first, the next page only when asked for;
each studyset opens a summary with all of it. Anonymous people explore
too.

Add to library makes a copy in the app, as an import: through the
person's own repositories, the files among their own files, the tags by
name among their own tags, everything read first, so a release in a
schema this app doesn't know is refused whole. The server never writes
into a learner's space. Every item of a copy names where it came from
(publication, item, release: card schema 9, note 3, step 3, path 2,
PDF 2, studyset 5), merged per side like the rest, for later releases to
be taken from there.

The Publish sheet takes a studyset off Explore again, after asking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:40 +02:00
1f995d4dc6
feat(explore): list published studysets, give out their releases, withdraw
Explore (ADR 0005, rule 4, stage 3) finds published studysets by fit,
never by popularity: search over titles and authors, the languages (a
primary language takes its regional ones), and the kinds of content they
hold, the newest release first, a page at a time. Each release now keeps
what Explore shows of it (title, description, languages, credits) beside
the repository, so the listing never reads git. Everyone signed in may
look and take a release's files (its JSON, read out of git in one
cat-file run, and each file by its hash) to make a copy of it.

An author can take a studyset off Explore again: nobody can add it then,
copies get no updates, its releases stay; publishing it again, changed
or not, brings it back.

The sync engine's test setup moves into test/integration/support, now
that two test files need it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:39 +02:00
f635b0816a
feat(publish): releases in git (public tier, stage 2)
A person with an identity and a public profile publishes a studyset with
everything inside it as its next release (ADR 0005, rule 3): the server
reads their synced rows and writes them through the git command as a
commit on main and the annotated tag vN, in a bare repository of the
studyset's own, named by a random public id made at the first publish.

A release is a directory per studyset with a file per card, note, path,
step and PDF (its id, position, tag names and payload as synced, keys
sorted and indented) and the files the content names, without what is
only the author's (a card's report, a PDF's deadline). Publishing is
refused without a licence allowing passing it on in every studyset ("no
derivatives" allowed), with revisions to settle, with files not uploaded
yet, when nothing changed, or for a name that won't do; the name is
fixed from the first release, as is the person's handle.

In the app, Share in a studyset's action row opens the Publish sheet;
publishing syncs first, and settles by itself the revisions that merge
cleanly, listing only real conflicts. The server's image gets git, the
repositories a volume of their own, and the hourly backup a git bundle of
each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:38 +02:00
7f6147d689
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-10 09:46:37 +02:00
b8462d650f
docs: ADR 0005, the public tier
Releases in a git repository per published studyset (the `git` command),
learning as a copy that follows (later releases offered, merged per side),
handles on the account domain Mastodon's way (`name@halkyon.eu`, changeable
until the first publish; a profile of display name, bio, links and
avatar), reports and suggestions of our own on plain git rather than a
forge's, carried by the sync engine, and ActivityPub through Fedify in a
container of its own: federated from the start, built in six usable
stages. Decided 2026-10-09; the alternatives and why they lost are in the
ADR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 09:46:30 +02:00
00ceffb36c
chore(release): v0.3.0
All checks were successful
CI / deploy (push) Successful in 22s
CI / check (push) Successful in 11m49s
v0.3.0
Build 6.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 19:31:29 +02:00
3bbdb9d747
chore: the pre-push hook refuses unsigned commits for main, as the deploy does
All checks were successful
CI / check (push) Successful in 11m56s
CI / deploy (push) Successful in 19s
deploy/redeploy.sh deploys main only if every new commit is signed by a key
in deploy/allowed-signers, and finds out after CI. On 2026-10-09 that held
learn.halkyon.eu at v0.2.2 for two days: 37 commits had reached main
unsigned, and every deploy since refused them. tool/check_signatures.sh
applies the same rule to a push, to the commits it brings to main that the
remote doesn't have yet (the list as the pushed commit has it), and
tool/check.sh runs it first when it runs as the pre-push hook: a second,
where the rest takes minutes. CI runs check.sh under its own name and
skips 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 19:14:15 +02:00
44cee0d6c6
feat: a dictionary's corrections reach the cards made from it, by offer
All checks were successful
CI / check (push) Successful in 12m33s
CI / deploy (push) Successful in 1m26s
Every dictionary entry now has a version: 16 hex digits of the SHA-256 of
what cards take of it (headword, reading, entry), made by the loader and
carried on every hit. A card keeps the version it took (card schema 8).
DictionaryEndpoint.changed returns, of the entries named with the
versions cards keep (1,000 at most a call; the app asks 500 at a time),
those whose version is another now.

The card browser says how many cards are behind their dictionary and takes
the corrections at a tap (decided 2026-10-09: by offer, never by
themselves). Taking one writes the entry as it is now over the revision
in which the card last took it, beside whatever came after, so the
revisions merge per side as two devices' edits do (ADR 0003): a side only
the dictionary changed takes the correction, a side the person changed
too is a conflict settled in the composer, whose words now name that
case. Cards made before versions are never offered one.

The server rereads its dictionary folder hourly now, not daily: the
dictionaries service fills its volume after an instance's first start,
and an hour with nothing changed costs a hash of each file.

Migration 20261009145434785-dictionary-entry-version: adds
dictionary_entry.version, text NOT NULL DEFAULT '' (rows loaded before
have none and are never reported as corrected), and by hand empties
dictionary_source."fileHash", so every source loads once more and its
entries get their versions; the schema stays as definition.sql says. The
old server goes on working until the switch.

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 18:09:37 +02:00
25e01ad128
feat(deploy): instances keep their dictionaries current, weekly, by themselves
EDRDG's licence asks of a server showing its dictionaries that it update
them at least monthly. The instance now does it alone: a `dictionaries`
service in compose, built from the curator (halkyon_learn_curator/
Dockerfile, its own unprivileged user 10001, 1 GB at most), runs `curate
update --every 7`, downloading EDRDG's files of the day once its files are
a week old and replacing them in a volume the server mounts read-only.
The curator never enters the server's image or process (decided
2026-10-09: a container of its own, weekly). The server rereads the
folder daily at 04:07 (DictionaryReloadFutureCall), loading what changed,
and loads now run one at a time. redeploy.sh builds both images.

An operator who'd rather supply the files removes the service and mounts
deploy/dictionaries/ into the server, as compose.yaml says.

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 18:09:37 +02:00
82b38fadd8
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.

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 18:09:36 +02:00
2d0ba20eb7
feat(curator): read JMdict as EDRDG publishes it, the XML of every day
EDRDG makes JMdict_e_examp.gz anew every day: JMdict in English with
Tatoeba's sentences on the meanings, the file jmdict-simplified's examples
edition is made from once a week. The curator now reads it directly (a
file not named .json is read as XML), streaming 72 MB of XML in some 30
seconds and 300 MB, each entry put into the shape jmdict-simplified gives
it and made an entry by the same code: against jmdict-simplified's
conversion of four days before, the entries differ only by JMdict's edits
since. JMdict's entities stay codes (`&v1;` is `v1`), text cut between two
chunks is joined before its entities are read (the xml package's
normalizeEvents), and a sense without parts of speech has the previous
sense's, as JMdict's DTD says. Its version is the day it was made.

So an instance can keep its dictionaries current from the source alone,
as EDRDG's licence asks of servers (at least monthly).

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 18:09:36 +02:00
9bb3826556
docs: credit KanjiVG in About, and name the kanji dictionary
About lists the data the app comes with beside the instance's
dictionaries: KanjiVG's strokes, by Ulrich Apel under CC BY-SA 3.0, with
the link its licence asks for. The server's README names the dictionary
of kanji and kana beside JMdict.

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 18:09:35 +02:00
89ad2c237d
feat(app): write kanji, kana and words by hand, each stroke checked
The study option Write by hand (settings schema 7) has character cards
and words written in a session, on a schedule of their own (review mode
`write`; older apps skip a mode they don't know): what to write on top, a
character's meanings and readings or a word's reading and meanings, and
below it a square to write it in, character by character.

Each stroke is checked as it ends against KanjiVG's: both resampled to 22
points, then their shape once centred, where they lie, how long they are
and which way they go, each against what a loose hand strays by; a stroke
closer to one still to come is out of order. One that counts is drawn as
KanjiVG draws it, and the hand's offset from the middle is forgiven in the
strokes after; any other flashes red, says why and counts as a mistake, as
does a hint. Two misses in a row show the next stroke faintly. Once
written, or given up on (Show it), the rating the mistakes suggest is
outlined: none Good, one or two OK, more Bad.

The square takes every touch on it at once, so writing never scrolls the
card or swipes to rate. Characters without a stroke order (in a word) are
written for the person.

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 18:09:34 +02:00
ed5bb3f6b1
feat(app): stroke order for kanji and kana, from KanjiVG
KanjiVG's strokes come with the app (assets/kanjivg/, CC BY-SA 3.0, its
notice in the licence list): 94 gzipped files, one per 256 code points,
2.7 MB in all, a file read the first time a character of it is asked for
(StrokeRepository, app-wide beside the maps). The strokes are SVG path
data, read with path_parsing (the Flutter team's, pure Dart, already in
the build through flutter_svg), so the model can flatten them into points
without dart:ui, as checking handwriting will.

A character's sheet traces its strokes in order over those to come, with
its stroke count, meanings, readings, level and the components KanjiVG
splits it into, each a tap away. It opens from a studied character's
answer (Stroke order) and from a character found in the dictionary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:34 +02:00
cec518350a
feat(app): kanji and kana in the dictionary, beside the words
Where the instance holds the characters of a studyset's language, the
dictionary screen has a tab for them, Kanji & kana: search by character,
reading (kana or romaji) or meaning, and whole levels to add, under their
names: the kana (hiragana, katakana), the JLPT levels and the school
grades. A character becomes a character card, its meanings (a kana's
romaji) and readings, tagged with its level (JLPT-N5), school grade
(Grade 1 … Grade 6, Secondary school) and script (Hiragana, Katakana). A
character card's Look up searches the characters, and an entry looked up
again on a card swaps the tags the one before brought, as words do.

A dictionary's entries are now words or characters (DictionaryItem), as
its kind says.

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 18:09:33 +02:00
2fc4205010
feat(app): a character card for kanji and kana
One card type for both (card schema 7): the character on the front, its
meanings and a kanji's on and kun readings, each merging on its own. A
kana's romaji is its meaning, so kana are studied, and later written,
exactly as kanji are. The composer offers it beside the other kinds, each
kind of reading on one line (ショク、ジキ); studying shows the character
large and, with the answer, its meanings and readings (the other way round
when asked from the back); the card browser filters by it and finds a kun
reading without its okurigana dot; the Markdown export writes its meanings
and readings.

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 18:09:33 +02:00
a6699b30cb
feat: dictionaries hold words or characters, and are asked by their source
A dictionary source now says what it holds (`kind`: `words`, the default
for files from before, or `characters`), a string so that an app older
than a new kind leaves it out. Search, countLabelled and labelled take the
dictionary by its source and meaning language, as the unique index names
it, rather than by the language: with KANJIDIC2 beside JMdict, a search
for words would otherwise bring kanji, and JLPT N5 would hold both. The
search reads the language for its query variants from the source's row;
a dictionary the instance doesn't hold finds nothing. The rank index goes
by source too.

Migration: drops and recreates dictionary_entry_rank_idx on (source,
glossLanguage, rank), and adds dictionary_source.kind, text NOT NULL
DEFAULT 'words', so a JMdict loaded before is a dictionary of words. Both
tables are reference data, never synced; the old server keeps working
until the switch (its queries by language just lose the index).

In the app, a studyset's dictionary is one of words unless a kind is
asked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:32 +02:00
e42cdfbfcb
feat(curator): KANJIDIC2 and the kana as a dictionary of characters; KanjiVG's strokes for the app
A dictionary file now says whether it holds words or characters (`kind`).
The kanjidic adapter turns KANJIDIC2 into one entry per kanji that has a
Japanese reading or an English meaning (12,633), with its readings,
meanings, school grade, strokes, frequency and JLPT level (Jonathan
Waller's, through kanji-data's kanji.json), labelled `jlpt:nX`,
`grade:N`/`grade:secondary`, followed by the 71 hiragana and katakana of
the kana table with their romaji. A kun reading is found whole; its kanji
part only as a loose key, so typing "ta" brings た before every kanji read
so.

`curate kanjivg` writes KanjiVG's strokes and components as the app's
stroke files, one gzipped JSON file per 256 code points, keyed by the code
point in each entry's id (KanjiVG's 橅 names itself 機).

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 18:09:32 +02:00
42c57bd40a
feat(app): a card browser to search, filter, sort and change many cards at once
Browse beside the flashcards' tab opens the studyset's cards over the
tabs (with everything inside it when it has sub-studysets), laid out as
the older design had it:
- a grid of tiles (each card's two sides) or one card at a time beside
  the list (on a phone above a strip of them, swiped through), as last
  left for the studyset on this device;
- a search over front, back, reading, meanings, hints, explanation and
  tag names, katakana found as hiragana and kana by romaji;
- sorting by own order, newest, oldest, A–Z (words by reading), due
  first; a filter sheet by kind, tag and state (new, learning, due,
  buried, reported);
- choosing cards (a toggle; shift-click or long-press for a range,
  select all) to move into another or a new studyset, add or remove
  tags, start afresh, or delete with Undo; the actions in the row on
  wide windows, in a bar at the foot below.

Moving cards between studysets is new in the card repository and its
gateway (studysetId and position written, named columns).

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 18:09:31 +02:00
8b38fb51fe
feat: a word's example sentences come along as its card's hints
The curator reads JMdict's examples edition (jmdict-simplified), which
ties Tatoeba's sentences to the meanings they show, and keeps them on
each sense of an entry (the form the word takes, the sentence, its
English, its Tatoeba id), crediting Tatoeba (CC BY 2.0 FR). A card made
from a word takes its first three as hints: the sentence with the word in
bold, the English below; Look up gives them to a card without hints of
its own. 98% of N5 and N4 words have one, 82% of N1. No new card format:
hints exist since card schema 5.

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 18:09:31 +02:00
dd36424917
feat(app): Anki's bracket furigana becomes readings where its templates show them
A field a note type shows through Anki's furigana filters
({{furigana:Reading}}, kanji:, kana:) holds 漢字[かんじ]; the import now
turns it into readings over the text before the brackets, as Anki's own
filters read it (back to a space, which goes). Other fields stay as
written; a sound stays a sound. The note types' templates and field
names are read from both package formats (Anki's unicase collation
registered so their tables can be read at all). New fixtures exported
by Anki itself (tool/anki/make_fixtures.py) test both formats.

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 18:09:30 +02:00
10ec88a435
perf(app): schedules replayed and revisions decoded only when they change
After every rating or save, Home, the library, a studyset's screen and
its lists each read every card and replayed FSRS for each, some of them
several times over (a progress per studyset above a card, six refreshes
for one change); at 2,500 cards that froze the screen for seconds.

- The review repository keeps each card's schedule with what it was
  replayed from and replays only cards whose reviews changed (2,500
  cards: 207 ms, then 32 ms after a rating, on the desktop); Home works
  it out from the reviews it has read already.
- The card repository decodes each revision once, and a watcher reads
  once more after a burst of changes rather than once per change.
- The library works progress out one run at a time, a burst of requests
  making one more run, reading each studyset's cards once per run and
  every schedule at once.
- Tag refreshes waiting in the queue are shared by the changes that
  asked for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:30 +02:00
084af278b5
fix(app): Reorder among older items, export and text import that hold up
- Reorder: rows from before an order existed all share one position, and
  one moved among them landed at their end; the run it goes into now
  gets positions of its own (placeAmong), for notes, files, paths,
  steps and sub-studysets. Rows whose deletion can still be undone count
  for nothing, as the list leaves them out, and a dropped row takes its
  place at once instead of snapping back until the order is read again.
- Export: two studysets inside each other (moved so on two devices
  offline) no longer make it run forever; links to files whose names
  hold #, % or brackets work; PDFs, images and sounds go into the zip
  without being compressed again; any error says the export failed.
- Text import: a real CSV reader (quoted fields over several lines, a
  back over several quoted fields, a stray quote spoiling only its own
  line), the separator guessed from the text (semicolons from German
  spreadsheets), and files in UTF-16 or Windows-1252 read as meant.
- A tab whose notes, paths or files couldn't be read says so rather than
  "No … yet"; an Anki import from the Library that fails leaves no empty
  studyset behind.

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 18:09:30 +02:00
d5e5945d93
fix(app): furigana stays on its word, and readings typed as learners type
Furigana:
- Text typed after a word with a reading no longer takes the reading.
- A reading over text formatted in parts (a bold kanji) is drawn once,
  over all of it, each part in its own style; in Markdown it is one
  <ruby>, the parts keeping their marks, the reading escaped.
- Readings follow the editor's text size, not the system's, and a
  linked word with a reading still opens its link.
- Anki's <ruby> keeps the formatting inside it.
- The Markdown export escapes what Markdown would read as its syntax
  and keeps blank lines inside code blocks.

Typing a reading:
- Hepburn as learners type it: onna is おんな, konnichiwa こんにちは, m
  before b, p and m is ん (shimbun); keyboards' n' and nnn still work.
- The caret stays where the person types, also in the middle.
- Macrons count both ways (ō as おう or おお); a card listing several
  readings (いく、ゆく) takes any of them; only words written with kanji
  ask for their reading.
- No autocorrect, suggestions or curly quotes in the field; Done with
  nothing typed keeps the keyboard; the lock screen lets go of the field.

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 18:09:29 +02:00
4aed71c8db
fix(app): glyph shapes, languages, licences and the side bar's foot
- A Japanese front keeps Japanese shapes whatever the back's language
  (Japanese learnt in Chinese or Korean took Chinese or Korean shapes).
- A change of languages no longer rebuilds the screen below: the scope
  keeps one shape, so a study session keeps its Undo.
- The PDF reader, with the cards and notes written beside it, takes its
  studyset's languages too.
- Licences: GPL-3.0 and AGPL-3.0 mean "only", as SPDX defines them (not
  "or later"); GPLv3 and "public domain" stay as typed; words around a
  Creative Commons licence ("International", "Unported", "Deed") and its
  name written out are read; 4.0 takes no jurisdiction.
- A new sub-studyset takes its parent's licence, not its holder or year.
- The year says it is wrong once typed whole or left; full-width digits
  count; the year's range comes from clock.
- New sub-studyset shows whole in the side bar, no longer cut at half.
- The languages switch says that neither side set means inheriting;
  "license" as the British verb.

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 18:09:29 +02:00
49e10895f4
fix(app): the dictionary adds each word once, says when it can't, and Look up tags hold
- A word tapped twice, or added while a level is being added, makes one
  card: adds run one at a time, and the word counts as in the studyset
  at once. The adder reads the studyset's cards once rather than for
  every word.
- Adding that fails says so, and a level shows "Making the cards…" once
  its words have come.
- Finding a studyset's dictionary tells finding, none and unreachable
  apart: the dictionary screen says there is none or offers Retry, and
  an instance unreachable at start is asked again once a sync goes
  through (Look up and From the dictionary appear then).
- Look up's tags wait on the card by name and are made when it is saved:
  leaving the card at once no longer saves it untagged, a word replaced
  leaves no tag behind, and a new word replaces any level or word class
  on the card, pinned ones too. Look up marks words already made into
  cards of the studyset.
- The tag picker's Create keeps the case typed.

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 18:09:28 +02:00
50ca46f6b1
fix: dictionary headwords, search order and a loader that can't crash
The curator:
- A word JMdict says is usually written in kana gets a kana headword,
  even where the JLPT lists give kanji (その, not 其の), and a rare or
  search-only spelling is never the headword while a usual reading
  exists (そして, not 而して); 9,611 headwords change, 96 of them N5.
- Keys hold full-width letters as typed (Tシャツ as tシャツ), デュ/テュ/
  フュ/イェ romanised, long a, i and e typed once as loose keys (kado for
  カード), and a blank JLPT line is skipped.

The server:
- A search is one query ordered by tier, then rank: a loose or prefix
  match no longer pushes an exact one out of the list (75-150 ms over
  all of JMdict, before 150-450).
- Queries find long vowels with macrons spelt おお or ー and typed once
  (ōkii, kōhī), ん typed n' or nn, and full-width letters.
- Pages of a label are ordered by rank and id, so entries that share a
  rank are neither repeated nor skipped.
- The loader never throws (an unreadable folder ended the server from
  the background), refuses two files of one source rather than loading
  both at every start, reads no rows back when replacing a source, and
  takes its time from clock.
- Dictionary files stay out of the Docker build context.

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 18:09:28 +02:00
d67dc8aa70
fix(app): Look up tags a word as the dictionary screen does
A vocabulary card filled through Look up in the composer got the word but
none of its tags, while the dictionary screen tags every word with its
JLPT level and word class. Both now take the tags from the word itself
(DictionaryWord.tagNames). Looking another word up on the same card swaps
the tags the word before brought; tags the person gave the card stay.

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 18:09:28 +02:00
72c15300ed
fix: Serverpod v4.0.4 and serverpod_offline_sync v0.0.10
serverpod_offline_sync v0.0.10 collects outbound changes in one
consistent snapshot, so a sync no longer fails on writes made while it
runs (upstream #84, #163), keeps checkpoints per author, so a device
clock moving backwards no longer stalls sync or resends data, and keeps
unique conflicts and foreign key defaults alike on every device. Its
tables and its messages are unchanged: no migration, and apps on v0.0.8
keep syncing. It needs Serverpod >=4.0.4 <4.1.0 (v4.0.4 is fixes only),
so the root overrides that held the workspace on one Serverpod go: each
package pins v4.0.4 itself. The generated code is identical under the
v4.0.4 CLI, which CI now uses.

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 18:09:27 +02:00
c20578e421
feat(app): tags keep their case; dictionary tags read JLPT-N5, Noun
A tag is still its name, but no longer in lowercase: the name keeps the
case it was written in, and names that differ only in case are one tag
(ADR 0004, rule 2, amended). Two such tags from two devices fold into the
one with the smaller id, its case with it. Tags stored before stay
lowercase until renamed.

Words from the dictionary are tagged JLPT-N5 to JLPT-N1 and with their
word class (Noun, Verb, i-Adjective, na-Adjective, ...). Anki tags and
PDF chapter tags arrive as written.

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 18:09:27 +02:00
02d6542cfa
feat(app): type a word's reading while studying
A new study option, Type the reading (settings schema 6, beside the
vocabulary quiz; turning one on turns the other off): a word with kanji
shows alone, and its reading is typed, in kana; romaji turn into kana as
they are typed (doubled consonants, n before a consonant, nn and n'), and
the device's own Japanese input works as it does. Check, or Enter, says
whether it was right (katakana counts as hiragana) and what was typed
beside the reading, then shows the meanings and the ratings; the person
still rates, as Anki's typed answers have it. A word written in kana, one
usually written so, and a card asked back first flip as before.

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 18:09:27 +02:00
30dde3b8fa
feat(app): furigana in rich text
Text within a line can carry its reading now (`ruby` on the text's
attributes, `かんじ` on `漢字`), word by word, in lessons, example
sentences, notes and cards alike. Shown, the reading sits above the text,
smaller and centred, the text level with the line around it; while
editing, the text is dotted underneath and the caret moves through it as
text, as with formulas. The format bar's Furigana asks for the reading of
the text selected, starting from the one it has, and takes it away.

An Anki field's `<ruby>` keeps its reading as furigana now, each base its
own, instead of "漢(かん)"; Anki's `漢字[かんじ]` stays as written, as
telling it apart from other brackets is guesswork. The Markdown export
writes furigana as HTML's `<ruby>漢字<rt>かんじ</rt></ruby>`.

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 18:09:26 +02:00
add1c76842
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.

- Add material › From the dictionary: search a word (kanji, kana,
  romaji, meaning) and add it as a flashcard, or add a whole level
  (JLPT N5…N1, common words) at once, asked first above 500 words, its
  progress shown. Words already in the studyset show as such and are
  left out. Each card is a vocabulary card (headword, reading, senses
  with their codes), tagged `jlpt-n5` and with its word class, and
  marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
  chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
  `dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
  asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:26 +02:00
7008fcac52
chore(server): generated code as the generator writes it
dart format had gone over the generated files too (a blank line between
import groups, other line breaks), which CI's check against a fresh
`serverpod generate` would have refused. AGENTS.md now says to format
only the files one wrote.

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 18:09:26 +02:00
3013bc04b3
feat(server): dictionaries, loaded from files and searched
The server now holds dictionaries for everyone: `dictionary_entry` and
`dictionary_source`, reference data owned by no one, never synced. It
loads the dictionary files in its `dictionaries` folder at every start,
those whose SHA-256 changed: in the background, each source replaced in
one transaction, so searches meanwhile find the old entries; a file it
can't read is logged and left out. All of JMdict loads in under half a
minute. On an instance the folder is deploy/dictionaries, mounted
read-only (compose.yaml); the server's README says how.

DictionaryEndpoint (signed in): the dictionaries with their credits; a
search (exact key, exact loose key, key starting with the query, loose
key starting with it, each in the source's order; katakana as hiragana,
macrons written out), some 150-450 ms over all of JMdict; and the
entries with a label, counted and a page at a time (all JLPT N5 words).

The curator now keeps spellings that aren't quite right apart, as
`looseKeys` (romaji with long vowels typed once): "sushi" finds 寿司
before 数詞 (すうし).

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 18:09:25 +02:00
a875a91f40
feat(curator): turn JMdict and the JLPT lists into dictionary files
A package of its own in the workspace, without Serverpod (AGPL, like the
tooling): an adapter per source writes one format for all, documented in
its README, which the server will load. The format is JSON Lines: a
header (source, version, languages, licence, credits), then entries with
a headword and reading, labels (`common`, `jlpt:n5`), a rank, the keys a
search matches, and the entry as cards take it. Everything specific to a
language happens in its adapter (Japanese: forms, readings in kana and
Hepburn romaji, long vowels typed once, meanings without "to" and
articles), so the server's search knows no language.

JMdict comes as jmdict-simplified's JSON, read one entry at a time: all
218,863 entries in some 20 seconds and 300 MB, 16.8 MB gzipped. The JLPT
lists (yomitan-jlpt-vocab, CC BY-SA 4.0, from Waller's CC BY lists) add
levels by JMdict entry, the easiest when a word is on several, and give
the headword its common spelling. Words rank N5 to N1, then the other
common words, then the rest.

The server's image leaves the package out, as it does the client's.

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 18:09:25 +02:00
bd6016ee1b
feat(app): choose a studyset's licence, its year and holder
The studyset sheet gains Licence, as before: "Help me choose" asks three
questions (keep any rights? commercial use? adaptations, alike?) and
recommends one of the six current Creative Commons licences or CC0, with
what it lets others do and a link to its terms; the licence field
suggests well-known licences and takes any other as typed, kept in one
spelling (`cc by-nc-sa 3.0 us` is `CC-BY-NC-SA-3.0-US`); the year sits at
its end (1000 to next year), the holder below. An older or ported
Creative Commons licence gets a note naming its current version. A new
sub-studyset starts with the licence of the one it is in.

Left out until publishing exists: the warnings about sub-studysets'
licences that a parent's doesn't cover. The licence shows for every
studyset, as there is no visibility 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-09 18:09:25 +02:00
b1ef9b9d3a
feat(app): a studyset's languages, and Chinese and Korean fonts
The studyset sheet gains Languages, as before: off until "Set
languages", then the language of the cards' fronts and of their backs
(22 languages, Chinese in its two scripts). A studyset without its own
takes those of the nearest studyset above it, both together; set for the
first time, they start from those it took. The payload held both fields
already.

They set its text: Noto Sans SC, TC and KR join Noto Sans JP (bundled,
never fetched; 34 MB more, fonts 42 MB in all, your call), and since
Han characters take the shapes of the first font that has them, every
screen inside a studyset (its screen, cards, notes, paths, studying it)
puts the font of its front's language first, else of its back's,
Japanese otherwise. Every studyset route goes through one helper now.

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 18:09:24 +02:00