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>
This commit is contained in:
parent
a6699b30cb
commit
2fc4205010
4 changed files with 765 additions and 18 deletions
|
|
@ -181,7 +181,7 @@ void main() {
|
|||
entry: (source: 'jmdict', id: '1358280'),
|
||||
);
|
||||
|
||||
expect(cardSchemaVersion, 6);
|
||||
expect(cardSchemaVersion, greaterThanOrEqualTo(6));
|
||||
expect(decodeCardPayload(encodeCardPayload(content)), content);
|
||||
expect(
|
||||
decodeCardPayload(
|
||||
|
|
@ -196,6 +196,39 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('a character keeps its meanings and its on and kun readings '
|
||||
'(schema 7); a card from before has no readings', () {
|
||||
final content = CardContent(
|
||||
type: CardType.character,
|
||||
front: text('食'),
|
||||
back: RichContent.empty(),
|
||||
meanings: const ['eat', 'food'],
|
||||
onReadings: const ['ショク', 'ジキ'],
|
||||
kunReadings: const ['く.う', 'た.べる'],
|
||||
entry: (source: 'kanjidic', id: '食'),
|
||||
);
|
||||
|
||||
expect(cardSchemaVersion, 7);
|
||||
expect(decodeCardPayload(encodeCardPayload(content)), content);
|
||||
expect(
|
||||
decodePayload(
|
||||
encodeCardPayload(content),
|
||||
newestSchema: cardSchemaVersion,
|
||||
),
|
||||
containsPair('on', ['ショク', 'ジキ']),
|
||||
);
|
||||
final before = decodeCardPayload(
|
||||
bytes({
|
||||
'schemaVersion': 6,
|
||||
'type': 'frontBack',
|
||||
'front': text('猫').toJson(),
|
||||
'back': text('cat').toJson(),
|
||||
}),
|
||||
);
|
||||
expect(before.onReadings, isEmpty);
|
||||
expect(before.kunReadings, isEmpty);
|
||||
});
|
||||
|
||||
test('reads a report of a category it does not know as "other"', () {
|
||||
final content = decodeCardPayload(
|
||||
bytes({
|
||||
|
|
|
|||
|
|
@ -74,6 +74,37 @@ void main() {
|
|||
expect(word.withField(CardField.meanings, const [' ']).isComplete, false);
|
||||
});
|
||||
|
||||
test('character: with the character and a meaning or a reading; a '
|
||||
"kana's romaji is its meaning", () {
|
||||
final kanji = CardContent(
|
||||
type: CardType.character,
|
||||
front: text('食'),
|
||||
back: none,
|
||||
onReadings: const ['ショク'],
|
||||
);
|
||||
final kana = CardContent(
|
||||
type: CardType.character,
|
||||
front: text('あ'),
|
||||
back: none,
|
||||
meanings: const ['a'],
|
||||
);
|
||||
|
||||
expect(kanji.isComplete, true);
|
||||
expect(kana.isComplete, true);
|
||||
expect(
|
||||
kanji.withField(CardField.onReadings, const <String>[]).isComplete,
|
||||
false,
|
||||
);
|
||||
expect(kana.withField(CardField.front, none).isComplete, false);
|
||||
expect(
|
||||
kanji
|
||||
.withField(CardField.onReadings, const <String>[])
|
||||
.withField(CardField.front, none)
|
||||
.isBlank,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('matching: with two targets and two items at least, every item in '
|
||||
'a target and no target empty', () {
|
||||
CardContent sorting(List<int> targets) => CardContent(
|
||||
|
|
@ -120,6 +151,24 @@ void main() {
|
|||
).answerPreview,
|
||||
'Tokyo',
|
||||
);
|
||||
expect(
|
||||
CardContent(
|
||||
type: CardType.character,
|
||||
front: text('食'),
|
||||
back: none,
|
||||
meanings: const ['eat', 'food'],
|
||||
onReadings: const ['ショク', 'ジキ'],
|
||||
kunReadings: const ['た.べる'],
|
||||
).answerPreview,
|
||||
'eat; food · ショク、ジキ · た.べる',
|
||||
);
|
||||
});
|
||||
|
||||
test("a character's readings, typed on one line, split at commas, "
|
||||
'Japanese commas and spaces', () {
|
||||
expect(splitReadings('ショク、ジキ'), ['ショク', 'ジキ']);
|
||||
expect(splitReadings(' く.う, た.べる は.む '), ['く.う', 'た.べる', 'は.む']);
|
||||
expect(splitReadings(''), isEmpty);
|
||||
});
|
||||
|
||||
test('uses only the fields of its type', () {
|
||||
|
|
@ -136,6 +185,15 @@ void main() {
|
|||
).fields,
|
||||
{CardField.front, CardField.answers, CardField.map},
|
||||
);
|
||||
expect(
|
||||
CardContent(type: CardType.character, front: none, back: none).fields,
|
||||
{
|
||||
CardField.front,
|
||||
CardField.meanings,
|
||||
CardField.onReadings,
|
||||
CardField.kunReadings,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('normalized drops blank hints and a blank explanation, and takes a '
|
||||
|
|
|
|||
|
|
@ -121,6 +121,40 @@ void main() {
|
|||
expect(find.text('cat'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a character shows its meanings and readings, and the filter '
|
||||
'finds characters alone', (tester) async {
|
||||
await cards.create(
|
||||
'japanese',
|
||||
CardContent(
|
||||
type: CardType.character,
|
||||
front: RichContent.plain('食'),
|
||||
back: RichContent.empty(),
|
||||
meanings: const ['eat'],
|
||||
onReadings: const ['ショク'],
|
||||
kunReadings: const ['た.べる'],
|
||||
),
|
||||
);
|
||||
await show(tester);
|
||||
|
||||
final answer = find.text('eat\nOn: ショク\nKun: た.べる');
|
||||
expect(answer, findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Filter'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Characters only'));
|
||||
await tester.tap(find.text('Apply'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('cat'), findsNothing);
|
||||
expect(answer, findsOneWidget);
|
||||
|
||||
// A kun reading is found without its okurigana dot.
|
||||
await tester.tap(find.byTooltip('Search'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'taberu');
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pumpAndSettle();
|
||||
expect(answer, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('cards chosen are deleted together, and Undo brings them '
|
||||
'back', (tester) async {
|
||||
await show(tester);
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ void main() {
|
|||
await showStudyset(tester, size: const Size(412, 915));
|
||||
await open(tester, '猫');
|
||||
|
||||
const types = ['Flashcard', 'Vocabulary', 'Matching', 'Map'];
|
||||
const types = ['Flashcard', 'Vocabulary', 'Matching', 'Map', 'Character'];
|
||||
for (final type in types) {
|
||||
final name = tester.renderObject<RenderParagraph>(find.text(type));
|
||||
expect(
|
||||
|
|
@ -528,6 +528,31 @@ void main() {
|
|||
expect(gateway.deletions, ['blank $id', 'delete $id']);
|
||||
});
|
||||
|
||||
testWidgets('a character card takes its meanings and its on and kun '
|
||||
'readings, each kind on one line', (tester) async {
|
||||
await cards.create('japanese', card('食', 'food'));
|
||||
await showStudyset(tester);
|
||||
await open(tester, '食');
|
||||
|
||||
await tester.tap(find.text('Character'));
|
||||
await settle(tester);
|
||||
expect(find.text('On readings'), findsOneWidget);
|
||||
expect(find.text('Kun readings'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField).at(0), 'eat');
|
||||
await tester.enterText(find.byType(TextField).at(1), 'ショク、ジキ');
|
||||
await tester.enterText(find.byType(TextField).at(2), 'く.う, た.べる');
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await settle(tester);
|
||||
|
||||
final saved = (await cards.cardsOf('japanese')).single.content;
|
||||
expect(saved.type, CardType.character);
|
||||
expect(saved.front.plainText, '食');
|
||||
expect(saved.meanings, ['eat']);
|
||||
expect(saved.onReadings, ['ショク', 'ジキ']);
|
||||
expect(saved.kunReadings, ['く.う', 'た.べる']);
|
||||
expect(saved.back, RichContent.empty());
|
||||
});
|
||||
|
||||
testWidgets('a map question takes the region tapped as its answer', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue