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>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-09 15:19:22 +02:00
commit cec518350a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -58,12 +58,36 @@ DictionaryWord word(
senses: [VocabularySense(glosses: glosses, pos: pos)],
);
/// A KANJIDIC2 character: [headword] meaning [meanings], read [on] and
/// [kun], with [labels]; a kana with its [romaji].
DictionaryCharacter character(
String headword, {
List<String> meanings = const [],
List<String> on = const [],
List<String> kun = const [],
List<String> labels = const [],
String? romaji,
int? grade,
int? strokes,
}) => DictionaryCharacter(
ref: (source: 'kanjidic', id: headword),
headword: headword,
labels: labels,
meanings: meanings,
onReadings: on,
kunReadings: kun,
romaji: romaji,
grade: grade,
strokes: strokes,
);
/// A [DictionaryGateway] in memory: an instance with the dictionaries
/// [available], each holding its [words] in their order. A search finds the words whose
/// headword, reading or a meaning starts with the query.
/// [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<DictionaryWord> words = const [],
final List<DictionaryItem> words = const [],
}) implements DictionaryGateway {
/// When set, every request fails with it.
Exception? fail;
@ -80,8 +104,12 @@ class FakeDictionaryGateway({
return available;
}
/// The entries of [dictionary].
Iterable<DictionaryItem> _of(Dictionary dictionary) =>
words.where((word) => word.ref.source == dictionary.source);
@override
Future<List<DictionaryWord>> search(
Future<List<DictionaryItem>> search(
Dictionary dictionary,
String query,
) async {
@ -90,29 +118,38 @@ class FakeDictionaryGateway({
}
final typed = query.trim().toLowerCase();
return [
for (final word in words)
if ([
word.headword,
?word.reading,
for (final sense in word.senses) ...sense.glosses,
].any((key) => key.toLowerCase().startsWith(typed)))
word,
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,
];
}
@override
Future<int> countLabelled(Dictionary dictionary, String label) async =>
words.where((word) => word.labels.contains(label)).length;
_of(dictionary).where((word) => word.labels.contains(label)).length;
@override
Future<List<DictionaryWord>> labelled(
Future<List<DictionaryItem>> labelled(
Dictionary dictionary,
String label, {
required int offset,
required int limit,
}) async => words
.where((word) => word.labels.contains(label))
.skip(offset)
.take(limit)
.toList();
}) async =>
_of(dictionary)
.where((word) => word.labels.contains(label))
.skip(offset)
.take(limit)
.toList();
}