halkyon/halkyon_learn_flutter/test/support/fake_dictionary_gateway.dart

356 lines
9.5 KiB
Dart
Raw Normal View History

import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
/// JMdict in English, as an instance that loaded it lists it.
const jmdict = Dictionary(
source: 'jmdict',
sourceVersion: '3.6.2 (2026-10-05)',
kind: DictionaryKind.words,
language: 'ja',
glossLanguage: 'en',
license: 'CC-BY-SA-4.0',
credits: [
(
name: 'JMdict',
by: 'Electronic Dictionary Research and Development Group (EDRDG)',
license: 'CC-BY-SA-4.0',
url: 'https://www.edrdg.org/edrdg/licence.html',
),
],
entryCount: 3,
);
/// KANJIDIC2 in English, with the kana, as an instance that loaded it lists
/// it.
const kanjidic = Dictionary(
source: 'kanjidic',
sourceVersion: '2026-282',
kind: DictionaryKind.characters,
language: 'ja',
glossLanguage: 'en',
license: 'CC-BY-SA-4.0',
credits: [
(
name: 'KANJIDIC2',
by: 'Electronic Dictionary Research and Development Group (EDRDG)',
license: 'CC-BY-SA-4.0',
url: 'https://www.edrdg.org/edrdg/licence.html',
),
],
entryCount: 2,
);
feat(dictionary): German, Greek, English, Chinese and Korean words A studyset in German, Greek, English, Chinese or Korean finds its dictionary as one in Japanese does (ADR 0008), and its subtitles are cut into words by its language (`WordCutting`): Chinese by its longest words as they stand, the longer words over each cut offered as for Japanese; Korean at its spaces, a word the dictionary lacks as it stands taken without the particle it ends with (집에서 is 집, 아이 stays a child); every other language at its spaces, a word written exactly so first (the noun Essen before the verb essen). A studyset finds the dictionary of its language tag, else of its language alone (zh for zh-Hans). A Wiktionary word shows its genders, principal forms, IPA, romanisation and Hanja; a German or Greek noun of one gender goes onto its card with its article (das Haus, το σπίτι); its tag is its word class, named in full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would read as JMdict's codes. Articles, pronouns, prepositions and conjunctions are left out of a video's words. Chinese words show their pinyin over each character on the Watch screen, carry their HSK level as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and by none for a language without. A reading is typed in a session only when it is kana, so pinyin isn't. Words are looked up on the web on Jisho in Japanese, on Wiktionary at the language's section otherwise, and a search's hint speaks of kana and romaji only in Japanese. `DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance keeps (unset: de,el,en,ko,zh; empty: none). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
/// German words with English meanings from Wiktionary, as an instance that
/// loaded them lists them.
const wiktionaryDe = Dictionary(
source: 'wiktionary-de',
sourceVersion: '2026-10-03',
kind: DictionaryKind.words,
language: 'de',
glossLanguage: 'en',
license: 'CC-BY-SA-4.0',
credits: [
(
name: 'Wiktionary',
by: 'Wiktionary contributors',
license: 'CC-BY-SA-4.0',
url: 'https://en.wiktionary.org/wiki/Wiktionary:Copyrights',
),
],
entryCount: 4,
);
/// Chinese words of CC-CEDICT in simplified characters.
const ccCedictHans = Dictionary(
source: 'cc-cedict-hans',
sourceVersion: '2026-10-14',
kind: DictionaryKind.words,
language: 'zh-Hans',
glossLanguage: 'en',
license: 'CC-BY-SA-4.0',
credits: [
(
name: 'CC-CEDICT',
by: 'MDBG',
license: 'CC-BY-SA-4.0',
url: 'https://www.mdbg.net/chinese/dictionary?page=cc-cedict',
),
],
entryCount: 0,
);
/// Chinese characters of Unihan in simplified writing.
const unihanHans = Dictionary(
source: 'unihan-hans',
sourceVersion: '18.0.0',
kind: DictionaryKind.characters,
language: 'zh-Hans',
glossLanguage: 'en',
license: 'Unicode-3.0',
credits: [
(
name: 'Unihan Database',
by: 'Unicode, Inc.',
license: 'Unicode-3.0',
url: 'https://www.unicode.org/reports/tr38/',
),
],
entryCount: 1,
);
/// Greek words with English meanings from Wiktionary.
const wiktionaryEl = Dictionary(
source: 'wiktionary-el',
sourceVersion: '2026-10-03',
kind: DictionaryKind.words,
language: 'el',
glossLanguage: 'en',
license: 'CC-BY-SA-4.0',
credits: [
(
name: 'Wiktionary',
by: 'Wiktionary contributors',
license: 'CC-BY-SA-4.0',
url: 'https://en.wiktionary.org/wiki/Wiktionary:Copyrights',
),
],
entryCount: 0,
);
/// The Greek alphabet the curator writes.
const greekAlphabet = Dictionary(
source: 'greek-alphabet',
sourceVersion: '1',
kind: DictionaryKind.characters,
language: 'el',
glossLanguage: 'en',
license: 'CC0-1.0',
credits: [
(
name: 'Alphabets (Hangul, Greek)',
by: 'Halkyon Learn contributors',
license: 'CC0-1.0',
url: 'https://code.halkyon.app/project-halkyon/halkyon_learn',
),
],
entryCount: 1,
);
feat(dictionary): German, Greek, English, Chinese and Korean words A studyset in German, Greek, English, Chinese or Korean finds its dictionary as one in Japanese does (ADR 0008), and its subtitles are cut into words by its language (`WordCutting`): Chinese by its longest words as they stand, the longer words over each cut offered as for Japanese; Korean at its spaces, a word the dictionary lacks as it stands taken without the particle it ends with (집에서 is 집, 아이 stays a child); every other language at its spaces, a word written exactly so first (the noun Essen before the verb essen). A studyset finds the dictionary of its language tag, else of its language alone (zh for zh-Hans). A Wiktionary word shows its genders, principal forms, IPA, romanisation and Hanja; a German or Greek noun of one gender goes onto its card with its article (das Haus, το σπίτι); its tag is its word class, named in full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would read as JMdict's codes. Articles, pronouns, prepositions and conjunctions are left out of a video's words. Chinese words show their pinyin over each character on the Watch screen, carry their HSK level as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and by none for a language without. A reading is typed in a session only when it is kana, so pinyin isn't. Words are looked up on the web on Jisho in Japanese, on Wiktionary at the language's section otherwise, and a search's hint speaks of kana and romaji only in Japanese. `DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance keeps (unset: de,el,en,ko,zh; empty: none). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
/// A German word of Wiktionary: [headword], its part of speech [pos] in
/// full, meaning [glosses], its [genders] and the [forms] that find it.
DictionaryWord germanWord(
String headword, {
required String pos,
List<String> glosses = const [],
List<String> genders = const [],
List<String> forms = const [],
List<String> labels = const [],
}) => DictionaryWord(
ref: (source: 'wiktionary-de', id: '$headword/$pos'),
headword: headword,
reading: null,
labels: labels,
senses: [
VocabularySense(glosses: glosses, pos: [pos]),
],
partOfSpeech: pos,
genders: genders,
inflections: [for (final form in forms) WordInflection(form, const [])],
language: 'de',
);
/// A JMdict word: [headword] read [reading], meaning [glosses], [labels]
/// and the parts of speech [pos] on its one sense.
DictionaryWord word(
String id,
String headword, {
String? reading,
List<String> glosses = const [],
List<String> labels = const [],
List<String> pos = const [],
feat: a dictionary's corrections reach the cards made from it, by offer 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 17:32:00 +02:00
String? version,
}) => DictionaryWord(
ref: (source: 'jmdict', id: id),
headword: headword,
reading: reading,
labels: labels,
senses: [VocabularySense(glosses: glosses, pos: pos)],
feat: a dictionary's corrections reach the cards made from it, by offer 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 17:32:00 +02:00
version: version,
);
/// A KANJIDIC2 character: [headword] meaning [meanings], read [on] and
/// [kun], with [labels]; a kana with its [romaji]; or a character [of]
/// another dictionary of characters.
DictionaryCharacter character(
String headword, {
List<String> meanings = const [],
List<String> on = const [],
List<String> kun = const [],
List<String> labels = const [],
String? romaji,
String? name,
int? grade,
int? strokes,
feat: a dictionary's corrections reach the cards made from it, by offer 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 17:32:00 +02:00
String? version,
Dictionary of = kanjidic,
}) => DictionaryCharacter(
ref: (source: of.source, id: headword),
name: name,
language: of.language,
headword: headword,
labels: labels,
meanings: meanings,
onReadings: on,
kunReadings: kun,
romaji: romaji,
grade: grade,
strokes: strokes,
feat: a dictionary's corrections reach the cards made from it, by offer 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 17:32:00 +02:00
version: version,
);
/// A [DictionaryGateway] in memory: an instance with the dictionaries
/// [available], holding the [words] (and characters) of each source in
/// their order. A search finds the entries whose headword, reading, a
/// meaning or a reading starts with the query.
class FakeDictionaryGateway({
final List<Dictionary> available = const [jmdict],
final List<DictionaryItem> words = const [],
}) implements DictionaryGateway {
/// When set, every request fails with it.
Exception? fail;
/// How often the dictionaries were asked for.
int asked = 0;
@override
Future<List<Dictionary>> dictionaries() async {
asked++;
if (fail case final error?) {
throw error;
}
return available;
}
/// The entries of [dictionary].
Iterable<DictionaryItem> _of(Dictionary dictionary) =>
words.where((word) => word.ref.source == dictionary.source);
@override
Future<List<DictionaryItem>> search(
Dictionary dictionary,
String query,
) async {
if (fail case final error?) {
throw error;
}
final typed = query.trim().toLowerCase();
return [
for (final item in _of(dictionary))
if (switch (item) {
DictionaryWord() => [
item.headword,
?item.reading,
for (final sense in item.senses) ...sense.glosses,
],
DictionaryCharacter() => [
item.headword,
...item.meaningsShown,
...item.onReadings,
for (final reading in item.kunReadings) reading.replaceAll('.', ''),
],
}.any((key) => key.toLowerCase().startsWith(typed)))
item,
];
}
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
@override
Future<List<DictionaryItem>> entries(
Dictionary dictionary,
List<String> ids,
) async {
if (fail case final error?) {
throw error;
}
final byId = {for (final item in _of(dictionary)) item.ref.id: item};
return [for (final id in ids) ?byId[id]];
}
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
/// How many keys [withKeys] was asked for each time.
final keysAsked = <int>[];
/// By its headword and its reading, as typed: the curator's keys hold
feat(dictionary): German, Greek, English, Chinese and Korean words A studyset in German, Greek, English, Chinese or Korean finds its dictionary as one in Japanese does (ADR 0008), and its subtitles are cut into words by its language (`WordCutting`): Chinese by its longest words as they stand, the longer words over each cut offered as for Japanese; Korean at its spaces, a word the dictionary lacks as it stands taken without the particle it ends with (집에서 is 집, 아이 stays a child); every other language at its spaces, a word written exactly so first (the noun Essen before the verb essen). A studyset finds the dictionary of its language tag, else of its language alone (zh for zh-Hans). A Wiktionary word shows its genders, principal forms, IPA, romanisation and Hanja; a German or Greek noun of one gender goes onto its card with its article (das Haus, το σπίτι); its tag is its word class, named in full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would read as JMdict's codes. Articles, pronouns, prepositions and conjunctions are left out of a video's words. Chinese words show their pinyin over each character on the Watch screen, carry their HSK level as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and by none for a language without. A reading is typed in a session only when it is kana, so pinyin isn't. Words are looked up on the web on Jisho in Japanese, on Wiktionary at the language's section otherwise, and a search's hint speaks of kana and romaji only in Japanese. `DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance keeps (unset: de,el,en,ko,zh; empty: none). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
/// both, the reading also in hiragana; a word of Wiktionary also by its
/// headword and inflections in lower case, as its text keys hold them.
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
@override
Future<Map<String, List<DictionaryItem>>> withKeys(
Dictionary dictionary,
List<String> keys,
) async {
if (fail case final error?) {
throw error;
}
keysAsked.add(keys.length);
final asked = keys.toSet();
final byKey = <String, List<DictionaryItem>>{};
for (final item in _of(dictionary)) {
for (final key in {
item.headword,
if (item case DictionaryWord(:final reading?)) reading,
feat(dictionary): German, Greek, English, Chinese and Korean words A studyset in German, Greek, English, Chinese or Korean finds its dictionary as one in Japanese does (ADR 0008), and its subtitles are cut into words by its language (`WordCutting`): Chinese by its longest words as they stand, the longer words over each cut offered as for Japanese; Korean at its spaces, a word the dictionary lacks as it stands taken without the particle it ends with (집에서 is 집, 아이 stays a child); every other language at its spaces, a word written exactly so first (the noun Essen before the verb essen). A studyset finds the dictionary of its language tag, else of its language alone (zh for zh-Hans). A Wiktionary word shows its genders, principal forms, IPA, romanisation and Hanja; a German or Greek noun of one gender goes onto its card with its article (das Haus, το σπίτι); its tag is its word class, named in full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would read as JMdict's codes. Articles, pronouns, prepositions and conjunctions are left out of a video's words. Chinese words show their pinyin over each character on the Watch screen, carry their HSK level as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and by none for a language without. A reading is typed in a session only when it is kana, so pinyin isn't. Words are looked up on the web on Jisho in Japanese, on Wiktionary at the language's section otherwise, and a search's hint speaks of kana and romaji only in Japanese. `DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance keeps (unset: de,el,en,ko,zh; empty: none). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:45:15 +02:00
if (item case DictionaryWord(partOfSpeech: _?, :final inflections)) ...[
item.headword.toLowerCase(),
for (final form in inflections) form.text.toLowerCase(),
],
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
}) {
if (asked.contains(key)) {
(byKey[key] ??= []).add(item);
}
}
}
return byKey;
}
feat: a dictionary's corrections reach the cards made from it, by offer 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 17:32:00 +02:00
/// How often [changed] was asked, and with how many entries each time.
final changedAsked = <int>[];
@override
Future<List<DictionaryItem>> changed(
Dictionary dictionary,
Map<String, String> versions,
) async {
if (fail case final error?) {
throw error;
}
changedAsked.add(versions.length);
return [
for (final item in _of(dictionary))
if (versions[item.ref.id] case final kept?)
if (item.version != null && item.version != kept) item,
];
}
@override
Future<int> countLabelled(Dictionary dictionary, String label) async {
if (fail case final error?) {
throw error;
}
return _of(dictionary).where((word) => word.labels.contains(label)).length;
}
@override
Future<List<DictionaryItem>> labelled(
Dictionary dictionary,
String label, {
required int offset,
required int limit,
}) async {
if (fail case final error?) {
throw error;
}
return _of(dictionary)
.where((word) => word.labels.contains(label))
.skip(offset)
.take(limit)
.toList();
}
}