feat(jellyfin): any stretch of a line's characters chosen by hand

The cut takes the longest word and knows no grammar, so 今日は reads as
こんにちは where it is 今日 and は. Any stretch of a line's characters can
now be chosen by hand: dragged across with a mouse, Shift+clicked from
the word or stretch chosen, or on a touch screen held on a character and
dragged on. It is looked up as one word, as it stands or as a form of
one, in one small request (the instance has the line's text already);
while it is chosen the line reads it as that word (きょう over 今日), and
what the dictionary lacks as one word is offered as a search on Jisho.

The line's words move into line_words.dart, out of the Watch screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-13 22:38:29 +02:00
commit 48ac84d92c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -600,6 +600,98 @@ void main() {
expect(card.content.entry, (source: 'jmdict', id: '37'));
});
group('a stretch chosen by hand (decided 2026-10-13: by characters)', () {
final greetings = [
word('40', '今日', reading: 'きょう', pos: ['n'], glosses: ['today']),
word('41', '今日は', reading: 'こんにちは', pos: ['int'], glosses: ['hello']),
word('42', 'は', pos: ['prt'], labels: ['common']),
word('43', '雨', reading: 'あめ', pos: ['n'], glosses: ['rain']),
word('44', 'だ', pos: ['cop'], labels: ['common']),
];
Future<void> watchGreetings(WidgetTester tester) async {
await watch(
tester,
vtt: {
('episode-1', 2): 'WEBVTT\n\n00:00:01.000 --> 00:00:05.000\n今日は雨だ\n',
('episode-1', 3):
'WEBVTT\n\n00:00:01.000 --> 00:00:05.000\nA grey sky.\n',
},
words: greetings,
);
await playTo(tester, const Duration(seconds: 2));
}
/// The middle of the [index]th character of the base text [text] of
/// the line (four characters to a run at most here, each as wide).
Offset character(WidgetTester tester, String text, int index) {
final rect = tester.getRect(find.text(text, findRichText: true).first);
final width = rect.width / text.length;
return Offset(rect.left + width * (index + 0.5), rect.center.dy);
}
testWidgets('dragged across with a mouse, the characters chosen are '
'looked up as one word, also inside a word the cut took longer: '
'今日 out of 今日は', (tester) async {
await watchGreetings(tester);
await players.last!.play();
expect(find.text('今日', findRichText: true), findsOneWidget);
final drag = await tester.startGesture(
character(tester, '今日', 0),
kind: PointerDeviceKind.mouse,
);
await drag.moveTo(character(tester, '今日', 1));
await drag.up();
await tester.pumpAndSettle();
expect(players.last!.state.value.playing, isFalse);
expect(find.textContaining('today', findRichText: true), findsOneWidget);
expect(find.textContaining('hello', findRichText: true), findsNothing);
// While chosen, the line reads it as the word chosen: きょう, not
// こんにち of こんにちは.
expect(find.text('こんにち'), findsNothing);
expect(find.text('きょう'), findsWidgets);
});
testWidgets('Shift+click chooses from the word chosen to the character '
'clicked; what the dictionary lacks as one word is searched on '
'Jisho', (tester) async {
await watchGreetings(tester);
await tester.tap(find.text('雨', findRichText: true));
await tester.pumpAndSettle();
expect(find.textContaining('rain', findRichText: true), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
final click = await tester.startGesture(
character(tester, 'だ', 0),
kind: PointerDeviceKind.mouse,
);
await click.up();
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.pumpAndSettle();
expect(
find.text("雨だ isn't in the dictionary as one word."),
findsOneWidget,
);
expect(find.text('Search Jisho for it'), findsOneWidget);
expect(find.textContaining('rain', findRichText: true), findsNothing);
});
testWidgets('on a phone, holding a character starts a stretch, and '
'moving on extends it', (tester) async {
await watchGreetings(tester);
final hold = await tester.startGesture(character(tester, '今日', 0));
await tester.pump(const Duration(milliseconds: 600));
await hold.moveTo(character(tester, '今日', 1));
await hold.up();
await tester.pumpAndSettle();
expect(find.textContaining('today', findRichText: true), findsOneWidget);
});
});
testWidgets("the episode's words can be studied or crammed: the cards of "
'its words, held before it was watched too, get its tag', (tester) async {
final station = sampleWords.firstWhere((word) => word.headword == '駅');