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>
2026-10-13 18:43:06 +02:00
|
|
|
import 'package:flutter/gestures.dart';
|
feat(jellyfin): the player as players are, with the settings under a cog
The Watch screen's player takes the shape people know from players,
YouTube's first: the picture set in from every side with rounded
corners, so the glow of its colours shows all around it, and over it
play or pause in the middle with 10 seconds back and on beside it, and
along its foot where the video is (0:05 ──── 23:47) and a cog. The
controls fade out while it plays and the pointer rests, and come back as
it moves, on a tap with a finger, on a jump and while paused. The cog
opens a panel inside the picture, so nothing runs off the window: Pause
after each line, the subtitles, the translation and the sound, each
track named by its language in the app's words, and the keyboard
shortcuts. The buttons for the line before, again and the next sit
beside the subtitles they act on, no longer looking like a player's
skip to the next video. Keys are YouTube's (Space or K, J and L, the
arrows, 0 to 9, Home, End) with Shift and the arrows for lines, R, P and
Esc; ? lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 16:51:53 +02:00
|
|
|
import 'package:flutter/services.dart';
|
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
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
|
|
|
|
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';
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
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';
|
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
|
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/widget/watch_screen.dart';
|
2026-10-13 19:32:37 +02:00
|
|
|
import 'package:halkyon_learn_flutter/src/feature/study/widget/study_screen.dart';
|
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
|
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
|
|
|
|
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';
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
import '../../support/fake_dictionary_gateway.dart';
|
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
|
|
|
import '../../support/fake_jellyfin.dart';
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
import '../../support/fake_review_gateway.dart';
|
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
|
|
|
import '../../support/fake_studyset_gateway.dart';
|
|
|
|
|
import '../../support/fake_tag_gateway.dart';
|
|
|
|
|
import '../../support/pump_app.dart';
|
|
|
|
|
import '../../support/sample_episode.dart';
|
|
|
|
|
import '../../support/test_dependencies.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late FakeCardGateway cardGateway;
|
|
|
|
|
late FakeTagGateway tagGateway;
|
|
|
|
|
late CardRepository cards;
|
|
|
|
|
late TagRepository tags;
|
|
|
|
|
late FakeJellyfinGateway jellyfin;
|
|
|
|
|
late FakeMediaPlayers players;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
cardGateway = FakeCardGateway();
|
|
|
|
|
tagGateway = FakeTagGateway();
|
|
|
|
|
cards = CardRepository(cardGateway);
|
|
|
|
|
tags = TagRepository(tagGateway);
|
|
|
|
|
jellyfin = sampleJellyfin();
|
|
|
|
|
players = FakeMediaPlayers();
|
|
|
|
|
});
|
|
|
|
|
tearDown(() async {
|
|
|
|
|
await cards.dispose();
|
|
|
|
|
await tags.dispose();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// Opens the sample episode from the Japanese studyset, in a window of
|
|
|
|
|
/// [size].
|
|
|
|
|
Future<void> watch(
|
|
|
|
|
WidgetTester tester, {
|
|
|
|
|
Size size = const Size(1400, 900),
|
|
|
|
|
bool playerAvailable = true,
|
|
|
|
|
JellyfinPlayback? playback,
|
|
|
|
|
Map<(String, int), String>? vtt,
|
|
|
|
|
List<MediaAudioTrack> audio = const [],
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
List<DictionaryWord>? words,
|
|
|
|
|
FakeReviewGateway? reviews,
|
feat(jellyfin): songs and their lyrics, read as subtitles are
A music library's artists and albums open as shows and seasons do, and
can be tied to a studyset. A song plays as sound alone (`/Audio/{id}/
stream`, as stored) with its lyrics as Jellyfin keeps them (`/Audio/{id}/
Lyrics`: an LRC file beside it, or its tags), cut into the dictionary's
words as a video's subtitles are, with no translation. Timed lyrics
follow the song: the line sung large and bright among the others faded,
scrolled to as it comes, a line tapped played from its start; untimed
ones show whole. A song without lyrics shows greyed and doesn't open
(`HasLyrics`), as there is nothing in it to read. Its cover stands where
the picture would, glowing as a video does, and its words are tagged
with the artist, shown, and "artist · title", hidden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 19:46:54 +02:00
|
|
|
FakeJellyfinGateway? gateway,
|
|
|
|
|
String itemId = 'episode-1',
|
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
|
|
|
}) async {
|
feat(jellyfin): songs and their lyrics, read as subtitles are
A music library's artists and albums open as shows and seasons do, and
can be tied to a studyset. A song plays as sound alone (`/Audio/{id}/
stream`, as stored) with its lyrics as Jellyfin keeps them (`/Audio/{id}/
Lyrics`: an LRC file beside it, or its tags), cut into the dictionary's
words as a video's subtitles are, with no translation. Timed lyrics
follow the song: the line sung large and bright among the others faded,
scrolled to as it comes, a line tapped played from its start; untimed
ones show whole. A song without lyrics shows greyed and doesn't open
(`HasLyrics`), as there is nothing in it to read. Its cover stands where
the picture would, glowing as a video does, and its words are tagged
with the artist, shown, and "artist · title", hidden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 19:46:54 +02:00
|
|
|
if (gateway != null) {
|
|
|
|
|
jellyfin = gateway;
|
|
|
|
|
}
|
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 (playback != null || vtt != null) {
|
|
|
|
|
jellyfin = sampleJellyfin(
|
|
|
|
|
playback: playback ?? sampleEpisodePlayback,
|
|
|
|
|
vtt:
|
|
|
|
|
vtt ??
|
|
|
|
|
{('episode-1', 2): sampleJapanese, ('episode-1', 3): sampleEnglish},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (audio.isNotEmpty) {
|
|
|
|
|
players = FakeMediaPlayers(audio: audio);
|
|
|
|
|
}
|
|
|
|
|
tester.view.physicalSize = size;
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
final studysets = FakeStudysetGateway()
|
|
|
|
|
..receiveRemoteChange(
|
|
|
|
|
'japanese',
|
|
|
|
|
encodeStudysetPayload(
|
|
|
|
|
StudysetContent(
|
|
|
|
|
title: 'Japanese',
|
|
|
|
|
description: RichContent.empty(),
|
|
|
|
|
frontLanguage: 'ja',
|
|
|
|
|
backLanguage: 'en',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
if (!playerAvailable) {
|
|
|
|
|
players = FakeMediaPlayers(available: false);
|
|
|
|
|
}
|
|
|
|
|
await pumpApp(
|
|
|
|
|
tester,
|
|
|
|
|
testDependencies(
|
|
|
|
|
studysetGateway: studysets,
|
|
|
|
|
cardGateway: cardGateway,
|
|
|
|
|
tagGateway: tagGateway,
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
dictionaryGateway: words == null
|
|
|
|
|
? sampleDictionary()
|
|
|
|
|
: FakeDictionaryGateway(words: words),
|
|
|
|
|
reviewGateway: reviews,
|
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
|
|
|
jellyfinGateway: jellyfin,
|
|
|
|
|
jellyfinStore: signedInJellyfin(),
|
|
|
|
|
mediaPlayers: players,
|
|
|
|
|
),
|
feat(jellyfin): songs and their lyrics, read as subtitles are
A music library's artists and albums open as shows and seasons do, and
can be tied to a studyset. A song plays as sound alone (`/Audio/{id}/
stream`, as stored) with its lyrics as Jellyfin keeps them (`/Audio/{id}/
Lyrics`: an LRC file beside it, or its tags), cut into the dictionary's
words as a video's subtitles are, with no translation. Timed lyrics
follow the song: the line sung large and bright among the others faded,
scrolled to as it comes, a line tapped played from its start; untimed
ones show whole. A song without lyrics shows greyed and doesn't open
(`HasLyrics`), as there is nothing in it to read. Its cover stands where
the picture would, glowing as a video does, and its words are tagged
with the artist, shown, and "artist · title", hidden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 19:46:54 +02:00
|
|
|
location: RoutePaths.watch('japanese', itemId),
|
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
|
|
|
);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Plays on to [position].
|
|
|
|
|
Future<void> playTo(WidgetTester tester, Duration position) async {
|
|
|
|
|
players.last!.moveTo(position);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
testWidgets('the line shown is cut into words, their readings above, its '
|
|
|
|
|
'translation beneath; it follows the video', (tester) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
expect(players.opened.single.headers['Authorization'], contains('token-1'));
|
|
|
|
|
expect(jellyfin.reports.single.started, isTrue);
|
|
|
|
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
expect(find.text('友達'), findsOneWidget);
|
|
|
|
|
expect(find.text('ともだち'), findsOneWidget);
|
|
|
|
|
// 会いました: 会 read あ, its ending as it stands.
|
|
|
|
|
expect(find.text('会'), findsOneWidget);
|
|
|
|
|
expect(find.text('あ'), findsOneWidget);
|
|
|
|
|
expect(find.text('いました'), findsOneWidget);
|
|
|
|
|
expect(find.text('Yesterday I met a friend at the station.'), findsOne);
|
|
|
|
|
|
|
|
|
|
// Between lines the last one stays.
|
|
|
|
|
await playTo(tester, const Duration(milliseconds: 5500));
|
|
|
|
|
expect(find.text('友達'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 12));
|
|
|
|
|
expect(find.text('旅'), findsOneWidget);
|
|
|
|
|
expect(find.text('友達'), findsNothing);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('a word tapped pauses the video and shows what it is a form '
|
|
|
|
|
'of; added, it is a flashcard with its line and translation as its '
|
|
|
|
|
'first hint, tagged with the show, and is marked from then on', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
await players.last!.play();
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('いました'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(players.last!.state.value.playing, isFalse);
|
|
|
|
|
expect(find.text('A form of 会う'), findsOneWidget);
|
|
|
|
|
await tester.tap(find.text('Add'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(find.text('会う added, with this line'), findsOneWidget);
|
|
|
|
|
final card = (await cards.cardsOf('japanese')).single;
|
|
|
|
|
expect(card.content.type, CardType.vocabulary);
|
|
|
|
|
expect(card.content.entry, (source: 'jmdict', id: '6'));
|
|
|
|
|
expect(
|
|
|
|
|
card.content.hints.first.plainText,
|
|
|
|
|
allOf(
|
|
|
|
|
contains('昨日、駅で友達に会いました。'),
|
|
|
|
|
contains('Yesterday I met a friend at the station.'),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final tagging = await tags.read();
|
|
|
|
|
expect(
|
|
|
|
|
[for (final tag in tagging.of(card.id)) tag.name],
|
2026-10-13 19:32:37 +02:00
|
|
|
containsAll(['Sample Journey', 'Sample Journey S1 E1', 'JLPT-N5']),
|
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
|
|
|
);
|
2026-10-13 19:32:37 +02:00
|
|
|
// The episode's own tag is hidden on cards; the show's is not.
|
|
|
|
|
expect(tagging.named('Sample Journey S1 E1')?.hidden, isTrue);
|
|
|
|
|
expect(tagging.named('Sample Journey')?.hidden, isFalse);
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
// How far it is learnt: not yet.
|
|
|
|
|
expect(find.text('New'), findsOneWidget);
|
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
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets("the video's words are listed, the most frequent first, "
|
|
|
|
|
'particles left out; one opens its first line', (tester) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
|
|
|
|
|
expect(find.text('Words in this video'), findsOneWidget);
|
|
|
|
|
final words = [
|
|
|
|
|
for (final tile in tester.widgetList<ListTile>(
|
|
|
|
|
find.descendant(
|
|
|
|
|
of: find.byType(CustomScrollView),
|
|
|
|
|
matching: find.byType(ListTile),
|
|
|
|
|
),
|
|
|
|
|
))
|
|
|
|
|
(tile.title! as Text).textSpan!.toPlainText().split(' ').first,
|
|
|
|
|
];
|
|
|
|
|
// 駅 and 友達 come twice; で, に, を, けど and だ are grammar.
|
|
|
|
|
expect(words.take(2).toSet(), {'駅', '友達'});
|
|
|
|
|
expect(words, isNot(contains('で')));
|
|
|
|
|
expect(words, contains('待つ'));
|
|
|
|
|
|
2026-10-13 19:32:37 +02:00
|
|
|
final wait = find.textContaining('待つ', findRichText: true).first;
|
|
|
|
|
await tester.ensureVisible(wait);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(wait);
|
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
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(players.last!.state.value.position, const Duration(seconds: 16));
|
|
|
|
|
expect(find.text('A form of 待つ'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
feat(jellyfin): the player as players are, with the settings under a cog
The Watch screen's player takes the shape people know from players,
YouTube's first: the picture set in from every side with rounded
corners, so the glow of its colours shows all around it, and over it
play or pause in the middle with 10 seconds back and on beside it, and
along its foot where the video is (0:05 ──── 23:47) and a cog. The
controls fade out while it plays and the pointer rests, and come back as
it moves, on a tap with a finger, on a jump and while paused. The cog
opens a panel inside the picture, so nothing runs off the window: Pause
after each line, the subtitles, the translation and the sound, each
track named by its language in the app's words, and the keyboard
shortcuts. The buttons for the line before, again and the next sit
beside the subtitles they act on, no longer looking like a player's
skip to the next video. Keys are YouTube's (Space or K, J and L, the
arrows, 0 to 9, Home, End) with Shift and the arrows for lines, R, P and
Esc; ? lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 16:51:53 +02:00
|
|
|
testWidgets('the next line and the one before play from their start, the '
|
|
|
|
|
'one shown from its start once under way; a line can end with a '
|
|
|
|
|
'pause', (tester) async {
|
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
|
|
|
await watch(tester);
|
|
|
|
|
await playTo(tester, const Duration(seconds: 7));
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.byTooltip('Next line'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(players.last!.state.value.position, const Duration(seconds: 11));
|
|
|
|
|
expect(players.last!.state.value.playing, isTrue);
|
|
|
|
|
|
feat(jellyfin): the player as players are, with the settings under a cog
The Watch screen's player takes the shape people know from players,
YouTube's first: the picture set in from every side with rounded
corners, so the glow of its colours shows all around it, and over it
play or pause in the middle with 10 seconds back and on beside it, and
along its foot where the video is (0:05 ──── 23:47) and a cog. The
controls fade out while it plays and the pointer rests, and come back as
it moves, on a tap with a finger, on a jump and while paused. The cog
opens a panel inside the picture, so nothing runs off the window: Pause
after each line, the subtitles, the translation and the sound, each
track named by its language in the app's words, and the keyboard
shortcuts. The buttons for the line before, again and the next sit
beside the subtitles they act on, no longer looking like a player's
skip to the next video. Keys are YouTube's (Space or K, J and L, the
arrows, 0 to 9, Home, End) with Shift and the arrows for lines, R, P and
Esc; ? lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 16:51:53 +02:00
|
|
|
// Under way: the one shown again, from its start.
|
|
|
|
|
await playTo(tester, const Duration(seconds: 14));
|
|
|
|
|
await tester.tap(find.byTooltip('Previous line'));
|
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
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(players.last!.state.value.position, const Duration(seconds: 11));
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.byTooltip('Previous line'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(players.last!.state.value.position, const Duration(seconds: 6));
|
|
|
|
|
|
feat(jellyfin): the player as players are, with the settings under a cog
The Watch screen's player takes the shape people know from players,
YouTube's first: the picture set in from every side with rounded
corners, so the glow of its colours shows all around it, and over it
play or pause in the middle with 10 seconds back and on beside it, and
along its foot where the video is (0:05 ──── 23:47) and a cog. The
controls fade out while it plays and the pointer rests, and come back as
it moves, on a tap with a finger, on a jump and while paused. The cog
opens a panel inside the picture, so nothing runs off the window: Pause
after each line, the subtitles, the translation and the sound, each
track named by its language in the app's words, and the keyboard
shortcuts. The buttons for the line before, again and the next sit
beside the subtitles they act on, no longer looking like a player's
skip to the next video. Keys are YouTube's (Space or K, J and L, the
arrows, 0 to 9, Home, End) with Shift and the arrows for lines, R, P and
Esc; ? lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 16:51:53 +02:00
|
|
|
await tester.tap(find.byTooltip('Settings'));
|
|
|
|
|
await tester.pumpAndSettle();
|
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
|
|
|
await tester.tap(find.text('Pause after each line'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await playTo(tester, const Duration(milliseconds: 9900));
|
|
|
|
|
expect(players.last!.state.value.playing, isFalse);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('leaving tells the server where the video stopped', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
await playTo(tester, const Duration(seconds: 12));
|
|
|
|
|
|
|
|
|
|
GoRouter.of(tester.element(find.byType(WatchScreen))).go(RoutePaths.home);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(jellyfin.reports.last.stopped, isTrue);
|
|
|
|
|
expect(jellyfin.reports.last.position, const Duration(seconds: 12));
|
|
|
|
|
expect(players.last!.disposed, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('without a player the subtitles still show, and the screen '
|
|
|
|
|
'says why the video does not', (tester) async {
|
|
|
|
|
await watch(tester, playerAvailable: false);
|
|
|
|
|
|
|
|
|
|
expect(find.textContaining("Videos can't play on this device"), findsOne);
|
|
|
|
|
expect(find.text('昨日'), findsNothing);
|
|
|
|
|
expect(find.text('Words in this video'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets("the sound is in the studyset's language, whichever track the "
|
|
|
|
|
'file plays first; the menu switches it', (tester) async {
|
|
|
|
|
await watch(
|
|
|
|
|
tester,
|
|
|
|
|
playback: const JellyfinPlayback(
|
|
|
|
|
itemId: 'episode-1',
|
|
|
|
|
mediaSourceId: 'episode-1',
|
|
|
|
|
playSessionId: 'session-1',
|
|
|
|
|
runTime: Duration(minutes: 24),
|
|
|
|
|
subtitles: [
|
|
|
|
|
JellyfinSubtitleTrack(index: 2, title: 'Japanese', language: 'jpn'),
|
|
|
|
|
JellyfinSubtitleTrack(index: 3, title: 'English', language: 'eng'),
|
|
|
|
|
],
|
|
|
|
|
audio: [
|
|
|
|
|
JellyfinAudioTrack(
|
|
|
|
|
index: 4,
|
|
|
|
|
title: 'German',
|
|
|
|
|
language: 'ger',
|
|
|
|
|
isDefault: true,
|
|
|
|
|
),
|
|
|
|
|
JellyfinAudioTrack(
|
|
|
|
|
index: 5,
|
|
|
|
|
title: 'English',
|
|
|
|
|
language: 'eng',
|
|
|
|
|
isDefault: true,
|
|
|
|
|
),
|
|
|
|
|
JellyfinAudioTrack(
|
|
|
|
|
index: 6,
|
|
|
|
|
title: 'Japanese [Original]',
|
|
|
|
|
language: 'jpn',
|
|
|
|
|
isDefault: true,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
// As Android names them.
|
|
|
|
|
audio: const [
|
|
|
|
|
MediaAudioTrack(id: 'a', language: 'de'),
|
|
|
|
|
MediaAudioTrack(id: 'b', language: 'en'),
|
|
|
|
|
MediaAudioTrack(id: 'c', language: 'ja'),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
expect(players.last!.selectedAudio?.id, 'c');
|
|
|
|
|
|
feat(jellyfin): the player as players are, with the settings under a cog
The Watch screen's player takes the shape people know from players,
YouTube's first: the picture set in from every side with rounded
corners, so the glow of its colours shows all around it, and over it
play or pause in the middle with 10 seconds back and on beside it, and
along its foot where the video is (0:05 ──── 23:47) and a cog. The
controls fade out while it plays and the pointer rests, and come back as
it moves, on a tap with a finger, on a jump and while paused. The cog
opens a panel inside the picture, so nothing runs off the window: Pause
after each line, the subtitles, the translation and the sound, each
track named by its language in the app's words, and the keyboard
shortcuts. The buttons for the line before, again and the next sit
beside the subtitles they act on, no longer looking like a player's
skip to the next video. Keys are YouTube's (Space or K, J and L, the
arrows, 0 to 9, Home, End) with Shift and the arrows for lines, R, P and
Esc; ? lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 16:51:53 +02:00
|
|
|
await tester.tap(find.byTooltip('Settings'));
|
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
|
|
|
await tester.pumpAndSettle();
|
feat(jellyfin): the player as players are, with the settings under a cog
The Watch screen's player takes the shape people know from players,
YouTube's first: the picture set in from every side with rounded
corners, so the glow of its colours shows all around it, and over it
play or pause in the middle with 10 seconds back and on beside it, and
along its foot where the video is (0:05 ──── 23:47) and a cog. The
controls fade out while it plays and the pointer rests, and come back as
it moves, on a tap with a finger, on a jump and while paused. The cog
opens a panel inside the picture, so nothing runs off the window: Pause
after each line, the subtitles, the translation and the sound, each
track named by its language in the app's words, and the keyboard
shortcuts. The buttons for the line before, again and the next sit
beside the subtitles they act on, no longer looking like a player's
skip to the next video. Keys are YouTube's (Space or K, J and L, the
arrows, 0 to 9, Home, End) with Shift and the arrows for lines, R, P and
Esc; ? lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 16:51:53 +02:00
|
|
|
// Tracks are named by their language, plainly.
|
|
|
|
|
expect(find.text('Japanese'), findsWidgets);
|
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
|
|
|
await tester.tap(find.text('Audio'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(find.text('German'));
|
|
|
|
|
await tester.pump();
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(players.last!.selectedAudio?.id, 'a');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets("a forced track, which only covers what the sound doesn't "
|
|
|
|
|
'say, is never the one read', (tester) async {
|
|
|
|
|
await watch(
|
|
|
|
|
tester,
|
|
|
|
|
playback: const JellyfinPlayback(
|
|
|
|
|
itemId: 'episode-1',
|
|
|
|
|
mediaSourceId: 'episode-1',
|
|
|
|
|
playSessionId: 'session-1',
|
|
|
|
|
runTime: Duration(minutes: 24),
|
|
|
|
|
subtitles: [
|
|
|
|
|
JellyfinSubtitleTrack(
|
|
|
|
|
index: 7,
|
|
|
|
|
title: 'Japanese (forced)',
|
|
|
|
|
language: 'jpn',
|
|
|
|
|
isDefault: true,
|
|
|
|
|
isForced: true,
|
|
|
|
|
),
|
|
|
|
|
JellyfinSubtitleTrack(index: 2, title: 'Japanese', language: 'jpn'),
|
|
|
|
|
JellyfinSubtitleTrack(index: 3, title: 'English', language: 'eng'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
|
|
|
|
|
expect(find.text('友達'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('a line takes the whole width, its translation on one line '
|
|
|
|
|
'too; the video keeps its size however long the line is', (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
final long = List.filled(8, '昨日、駅で友達に会いました。').join();
|
|
|
|
|
await watch(
|
|
|
|
|
tester,
|
|
|
|
|
vtt: {
|
|
|
|
|
('episode-1', 2):
|
|
|
|
|
'WEBVTT\n\n00:00:01.000 --> 00:00:05.000\n旅\n\n'
|
|
|
|
|
'00:00:06.000 --> 00:00:10.000\n$long\n',
|
|
|
|
|
('episode-1', 3):
|
|
|
|
|
'WEBVTT\n\n00:00:01.000 --> 00:00:05.000\nA journey.\n\n'
|
|
|
|
|
'00:00:06.000 --> 00:00:10.000\n'
|
|
|
|
|
'So I say, "Here you go!",\nand I lent it to him.\n',
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
final controls = tester.getTopLeft(find.byTooltip('Previous line'));
|
|
|
|
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 7));
|
|
|
|
|
expect(
|
|
|
|
|
find.text('So I say, "Here you go!", and I lent it to him.'),
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
);
|
|
|
|
|
expect(tester.getTopLeft(find.byTooltip('Previous line')), controls);
|
|
|
|
|
expect(tester.takeException(), isNull);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-13 16:35:44 +02:00
|
|
|
testWidgets('resizing the window, across every size class, keeps the '
|
|
|
|
|
'video where it is', (tester) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
await playTo(tester, const Duration(minutes: 2));
|
|
|
|
|
final playing = players.last;
|
|
|
|
|
|
|
|
|
|
for (final size in const [
|
|
|
|
|
Size(800, 900),
|
|
|
|
|
Size(500, 900),
|
|
|
|
|
Size(1400, 900),
|
|
|
|
|
]) {
|
|
|
|
|
tester.view.physicalSize = size;
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(players.opened, hasLength(1));
|
|
|
|
|
expect(players.last, same(playing));
|
|
|
|
|
expect(playing!.disposed, isFalse);
|
|
|
|
|
expect(playing.state.value.position, const Duration(minutes: 2));
|
|
|
|
|
});
|
|
|
|
|
|
feat(jellyfin): the player as players are, with the settings under a cog
The Watch screen's player takes the shape people know from players,
YouTube's first: the picture set in from every side with rounded
corners, so the glow of its colours shows all around it, and over it
play or pause in the middle with 10 seconds back and on beside it, and
along its foot where the video is (0:05 ──── 23:47) and a cog. The
controls fade out while it plays and the pointer rests, and come back as
it moves, on a tap with a finger, on a jump and while paused. The cog
opens a panel inside the picture, so nothing runs off the window: Pause
after each line, the subtitles, the translation and the sound, each
track named by its language in the app's words, and the keyboard
shortcuts. The buttons for the line before, again and the next sit
beside the subtitles they act on, no longer looking like a player's
skip to the next video. Keys are YouTube's (Space or K, J and L, the
arrows, 0 to 9, Home, End) with Shift and the arrows for lines, R, P and
Esc; ? lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 16:51:53 +02:00
|
|
|
testWidgets('the player has play or pause with 10 seconds back and on '
|
|
|
|
|
'beside it, and where it is from 0:00 to its length', (tester) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
await playTo(tester, const Duration(minutes: 1));
|
|
|
|
|
expect(find.text('1:00'), findsOneWidget);
|
|
|
|
|
expect(find.text('24:00'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.byTooltip('10 seconds on'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(players.last!.state.value.position, const Duration(seconds: 70));
|
|
|
|
|
await tester.tap(find.byTooltip('10 seconds back'));
|
|
|
|
|
await tester.tap(find.byTooltip('10 seconds back'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(players.last!.state.value.position, const Duration(seconds: 50));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets("YouTube's keys work, the lines' too, and ? lists them", (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
await playTo(tester, const Duration(minutes: 1));
|
|
|
|
|
Duration at() => players.last!.state.value.position;
|
|
|
|
|
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyL);
|
|
|
|
|
expect(at(), const Duration(seconds: 70));
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyJ);
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
|
|
|
expect(at(), const Duration(seconds: 55));
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.digit5);
|
|
|
|
|
expect(at(), const Duration(minutes: 12));
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.home);
|
|
|
|
|
expect(at(), Duration.zero);
|
|
|
|
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 7));
|
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
|
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(at(), const Duration(seconds: 11));
|
|
|
|
|
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyP);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await playTo(tester, const Duration(milliseconds: 14900));
|
|
|
|
|
expect(players.last!.state.value.playing, isFalse);
|
|
|
|
|
|
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
|
|
|
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
expect(find.text('Keyboard shortcuts'), findsOneWidget);
|
|
|
|
|
expect(find.text('Previous or next line'), findsOneWidget);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
testWidgets('the controls fade out while it plays and the pointer rests, '
|
|
|
|
|
'and come back while paused', (tester) async {
|
|
|
|
|
await watch(tester);
|
|
|
|
|
double shown() => tester
|
|
|
|
|
.widget<AnimatedOpacity>(
|
|
|
|
|
find
|
|
|
|
|
.ancestor(
|
|
|
|
|
of: find.byTooltip('10 seconds on'),
|
|
|
|
|
matching: find.byType(AnimatedOpacity),
|
|
|
|
|
)
|
|
|
|
|
.first,
|
|
|
|
|
)
|
|
|
|
|
.opacity;
|
|
|
|
|
expect(shown(), 1);
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.byTooltip('Play'));
|
|
|
|
|
await tester.pump();
|
|
|
|
|
await tester.pump(const Duration(seconds: 4));
|
|
|
|
|
expect(shown(), 0);
|
|
|
|
|
|
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
|
|
|
await tester.pump();
|
|
|
|
|
expect(shown(), 1);
|
|
|
|
|
});
|
|
|
|
|
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
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),
|
|
|
|
|
);
|
2026-10-13 19:23:17 +02:00
|
|
|
await tester.pump(const Duration(milliseconds: 700));
|
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>
2026-10-13 18:43:06 +02:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
fix(dictionary): a long kana word stands, and a word offers the longer ones
Cutting a line into words, kana that is no common word gave way to a
shorter common one whatever its length, so もうろく (耄碌, senility) was
taken as もう and ろく. The rule now holds only for kana of up to three
characters (が and まだ still win over がま, a cattail, and だ); a longer
kana word stands even when uncommon, as ichi.moe's segmenter weighs long
kana words by their length. And as no cut is always right, each word now
knows the longer words running over it into its neighbours, worked out
from the same lookup, which the Watch screen's panel offers beneath its
entries ("Longer words here"), each to add with the line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 19:04:38 +02:00
|
|
|
testWidgets('a word tapped also offers the longer words running over it, '
|
|
|
|
|
'to add with the line', (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\nStill.\n',
|
|
|
|
|
},
|
|
|
|
|
words: [
|
|
|
|
|
word('31', 'が', pos: ['prt'], labels: ['common']),
|
|
|
|
|
word('37', 'がま', pos: ['n'], glosses: ['bulrush']),
|
|
|
|
|
word('38', 'まだ', pos: ['adv'], labels: ['common']),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
await tester.tap(find.text('が'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
expect(find.text('Longer words here'), findsOneWidget);
|
|
|
|
|
expect(find.textContaining('bulrush', findRichText: true), findsOneWidget);
|
|
|
|
|
await tester.tap(find.text('Add').last);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
final card = (await cards.cardsOf('japanese')).single;
|
|
|
|
|
expect(card.content.entry, (source: 'jmdict', id: '37'));
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-13 19:32:37 +02:00
|
|
|
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 == '駅');
|
|
|
|
|
await DictionaryAdder(cards, tags).add('japanese', [station]);
|
|
|
|
|
final card = (await cards.cardsOf('japanese')).single;
|
|
|
|
|
await watch(tester);
|
|
|
|
|
await tester.tap(find.byTooltip('Play'));
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Study'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(find.text('Cram, each once'));
|
|
|
|
|
await tester.pump();
|
|
|
|
|
await tester.pump(const Duration(seconds: 1));
|
|
|
|
|
|
|
|
|
|
final tagging = await tags.read();
|
|
|
|
|
expect(
|
|
|
|
|
[for (final tag in tagging.of(card.id)) tag.name],
|
|
|
|
|
contains('Sample Journey S1 E1'),
|
|
|
|
|
);
|
|
|
|
|
expect(players.last!.state.value.playing, isFalse);
|
|
|
|
|
// The session, over the Watch screen, on the episode's tag, crammed (a
|
|
|
|
|
// spinner turns there: frames, not settling).
|
|
|
|
|
await tester.pump();
|
|
|
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
|
|
|
final location = GoRouterState.of(
|
|
|
|
|
tester.element(find.byType(StudyScreen)),
|
|
|
|
|
).uri;
|
|
|
|
|
expect(location.path, '/study/japanese');
|
|
|
|
|
expect(location.queryParameters, containsPair('cram', '1'));
|
|
|
|
|
expect(
|
|
|
|
|
location.queryParameters,
|
|
|
|
|
containsPair('tagName', 'Sample Journey S1 E1'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
feat(jellyfin): songs and their lyrics, read as subtitles are
A music library's artists and albums open as shows and seasons do, and
can be tied to a studyset. A song plays as sound alone (`/Audio/{id}/
stream`, as stored) with its lyrics as Jellyfin keeps them (`/Audio/{id}/
Lyrics`: an LRC file beside it, or its tags), cut into the dictionary's
words as a video's subtitles are, with no translation. Timed lyrics
follow the song: the line sung large and bright among the others faded,
scrolled to as it comes, a line tapped played from its start; untimed
ones show whole. A song without lyrics shows greyed and doesn't open
(`HasLyrics`), as there is nothing in it to read. Its cover stands where
the picture would, glowing as a video does, and its words are tagged
with the artist, shown, and "artist · title", hidden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 19:46:54 +02:00
|
|
|
testWidgets("a song's lyrics move with it, its words looked up as a "
|
|
|
|
|
"video's; a word added is tagged with the artist and the song", (
|
|
|
|
|
tester,
|
|
|
|
|
) async {
|
|
|
|
|
await watch(tester, gateway: sampleMusic(), itemId: 'song-1');
|
|
|
|
|
expect(find.text('Words in this song'), findsOneWidget);
|
|
|
|
|
// Every line shows, cut into words; the empty one is left out.
|
|
|
|
|
expect(find.text('友達'), findsOneWidget);
|
|
|
|
|
expect(find.text('読'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
await tester.tap(find.text('駅'));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
await tester.tap(find.text('Add').first);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
final card = (await cards.cardsOf('japanese')).single;
|
|
|
|
|
final tagging = await tags.read();
|
|
|
|
|
expect(
|
|
|
|
|
[for (final tag in tagging.of(card.id)) tag.name],
|
|
|
|
|
containsAll(['Sample Artist', 'Sample Artist · Sample Song']),
|
|
|
|
|
);
|
|
|
|
|
expect(tagging.named('Sample Artist · Sample Song')?.hidden, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
expect(find.text('友達'), findsOneWidget);
|
|
|
|
|
expect(tester.takeException(), isNull);
|
|
|
|
|
});
|
|
|
|
|
}
|