feat(jellyfin): the words' panel unfolds senses, shows progress, filters
The panel beside the video lists each entry's senses one to a line, as Jisho does (and as every dictionary list in the app now does), folded to two lines; an entry unfolds over the list, not in it, after a moment under the pointer or on a tap, in the video's colour, so nothing moves under the pointer and words of many senses (上げる has 25) crowd out nothing. Each entry links to the word on Jisho.org, opened only when tapped. A word the studyset holds shows how far its card is learnt: New, Learning, Due, or Learned with when it comes again. The video's words go through a filter in place of New only: new to the studyset, held or all, JLPT levels, and word classes. The dictionary's sources stay in the panel's lower right corner, and the player's controls hide after 2 seconds rather than 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
48a9564231
commit
fbe53c3441
1 changed files with 1019 additions and 113 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -6,6 +7,9 @@ import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/video/media_players.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/vocabulary_senses.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_adder.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/jellyfin/widget/watch_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
|
|
@ -14,7 +18,9 @@ import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
|||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_dictionary_gateway.dart';
|
||||
import '../../support/fake_jellyfin.dart';
|
||||
import '../../support/fake_review_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/fake_tag_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
|
|
@ -51,6 +57,8 @@ void main() {
|
|||
JellyfinPlayback? playback,
|
||||
Map<(String, int), String>? vtt,
|
||||
List<MediaAudioTrack> audio = const [],
|
||||
List<DictionaryWord>? words,
|
||||
FakeReviewGateway? reviews,
|
||||
}) async {
|
||||
if (playback != null || vtt != null) {
|
||||
jellyfin = sampleJellyfin(
|
||||
|
|
@ -87,7 +95,10 @@ void main() {
|
|||
studysetGateway: studysets,
|
||||
cardGateway: cardGateway,
|
||||
tagGateway: tagGateway,
|
||||
dictionaryGateway: sampleDictionary(),
|
||||
dictionaryGateway: words == null
|
||||
? sampleDictionary()
|
||||
: FakeDictionaryGateway(words: words),
|
||||
reviewGateway: reviews,
|
||||
jellyfinGateway: jellyfin,
|
||||
jellyfinStore: signedInJellyfin(),
|
||||
mediaPlayers: players,
|
||||
|
|
@ -160,7 +171,8 @@ void main() {
|
|||
[for (final tag in tagging.of(card.id)) tag.name],
|
||||
containsAll(['Sample Journey', 'JLPT-N5']),
|
||||
);
|
||||
expect(find.byTooltip('In this studyset'), findsOneWidget);
|
||||
// How far it is learnt: not yet.
|
||||
expect(find.text('New'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("the video's words are listed, the most frequent first, "
|
||||
|
|
@ -449,6 +461,103 @@ void main() {
|
|||
expect(shown(), 1);
|
||||
});
|
||||
|
||||
testWidgets('a word the studyset holds says how far it is learnt, in the '
|
||||
'list of the words it holds', (tester) async {
|
||||
final station = sampleWords.firstWhere((word) => word.headword == '駅');
|
||||
await DictionaryAdder(cards, tags).add('japanese', [station]);
|
||||
final card = (await cards.cardsOf('japanese')).single;
|
||||
final reviews = FakeReviewGateway();
|
||||
await reviews.insert([
|
||||
(
|
||||
id: '',
|
||||
cardId: card.id,
|
||||
rating: 4,
|
||||
reviewedAt: DateTime.now().toUtc().subtract(const Duration(days: 1)),
|
||||
durationMs: 3000,
|
||||
mode: 'review',
|
||||
),
|
||||
]);
|
||||
await watch(tester, reviews: reviews);
|
||||
|
||||
await tester.tap(find.byTooltip('Filter the words'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Words in the studyset'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('駅', findRichText: true), findsOneWidget);
|
||||
expect(find.textContaining('Learned · again'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the filter narrows the words by their class', (tester) async {
|
||||
await watch(tester);
|
||||
expect(find.textContaining('駅', findRichText: true), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Filter the words'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Verbs'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('会う', findRichText: true), findsOneWidget);
|
||||
expect(find.textContaining('駅', findRichText: true), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a word of many senses unfolds over the list under the '
|
||||
'pointer, a tap keeping it open; senses one to a line', (tester) async {
|
||||
final long = [
|
||||
for (final word in sampleWords)
|
||||
if (word.headword == '駅')
|
||||
DictionaryWord(
|
||||
ref: word.ref,
|
||||
headword: word.headword,
|
||||
reading: word.reading,
|
||||
labels: word.labels,
|
||||
senses: [
|
||||
for (var n = 1; n <= 6; n++)
|
||||
VocabularySense(
|
||||
glosses: ['station, as in sense $n of a long entry'],
|
||||
pos: const ['n'],
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
word,
|
||||
];
|
||||
await watch(tester, words: long);
|
||||
await playTo(tester, const Duration(seconds: 2));
|
||||
await tester.tap(find.text('駅'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(VocabularySensesView), findsNothing);
|
||||
|
||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(mouse.removePointer);
|
||||
await mouse.addPointer(location: Offset.zero);
|
||||
await mouse.moveTo(
|
||||
tester.getCenter(find.textContaining('sense 1 of a long').first),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(VocabularySensesView), findsOneWidget);
|
||||
expect(find.text('station, as in sense 6 of a long entry'), findsOneWidget);
|
||||
|
||||
// Left, it folds again.
|
||||
await mouse.moveTo(Offset.zero);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(VocabularySensesView), findsNothing);
|
||||
|
||||
// Tapped, it stays open with the pointer elsewhere; a tap on it closes
|
||||
// it.
|
||||
await tester.tap(find.textContaining('sense 1 of a long').first);
|
||||
await tester.pumpAndSettle();
|
||||
await mouse.moveTo(Offset.zero);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(VocabularySensesView), findsOneWidget);
|
||||
await tester.tap(find.byType(VocabularySensesView));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(VocabularySensesView), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('on a phone the words are below the video', (tester) async {
|
||||
await watch(tester, size: const Size(412, 915));
|
||||
await playTo(tester, const Duration(seconds: 2));
|
||||
|
|
|
|||
Loading…
Reference in a new issue