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:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_suggestion.dart';
|
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.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/widget/watch_screen.dart';
|
|
|
|
|
|
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:material_ui/material_ui.dart';
|
|
|
|
|
|
|
|
|
|
|
|
import '../../support/fake_jellyfin.dart';
|
|
|
|
|
|
import '../../support/fake_studyset_gateway.dart';
|
|
|
|
|
|
import '../../support/pump_app.dart';
|
|
|
|
|
|
import '../../support/sample_episode.dart';
|
|
|
|
|
|
import '../../support/test_dependencies.dart';
|
|
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
|
late FakeJellyfinGateway jellyfin;
|
|
|
|
|
|
late MemoryJellyfinStore store;
|
|
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
|
jellyfin = sampleJellyfin();
|
|
|
|
|
|
store = MemoryJellyfinStore();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
Future<void> show(
|
|
|
|
|
|
WidgetTester tester,
|
|
|
|
|
|
String location, {
|
|
|
|
|
|
String? suggested,
|
|
|
|
|
|
}) 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
|
|
|
|
tester.view.physicalSize = const Size(1200, 1400);
|
|
|
|
|
|
tester.view.devicePixelRatio = 1;
|
|
|
|
|
|
addTearDown(tester.view.reset);
|
|
|
|
|
|
final studysets = FakeStudysetGateway()
|
|
|
|
|
|
..receiveRemoteChange(
|
|
|
|
|
|
'japanese',
|
|
|
|
|
|
encodeStudysetPayload(
|
|
|
|
|
|
StudysetContent(
|
|
|
|
|
|
title: 'Japanese',
|
|
|
|
|
|
description: RichContent.empty(),
|
|
|
|
|
|
frontLanguage: 'ja',
|
|
|
|
|
|
backLanguage: 'en',
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
await pumpApp(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
testDependencies(
|
|
|
|
|
|
studysetGateway: studysets,
|
|
|
|
|
|
dictionaryGateway: sampleDictionary(),
|
|
|
|
|
|
jellyfinGateway: jellyfin,
|
|
|
|
|
|
jellyfinStore: store,
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
jellyfinSuggestion: JellyfinSuggestion(() async => suggested),
|
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
|
|
|
|
),
|
|
|
|
|
|
location: location,
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Finds the server, from Settings or the studyset.
|
|
|
|
|
|
Future<void> findServer(WidgetTester tester) async {
|
|
|
|
|
|
await tester.enterText(
|
|
|
|
|
|
find.widgetWithText(TextField, 'Server address'),
|
|
|
|
|
|
'jellyfin.local:8920',
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('Settings connects a server over HTTPS with a name and '
|
|
|
|
|
|
'password, kept on the device, and signs out of it', (tester) async {
|
|
|
|
|
|
await show(tester, RoutePaths.settings);
|
|
|
|
|
|
await tester.scrollUntilVisible(
|
|
|
|
|
|
find.widgetWithText(TextField, 'Server address'),
|
|
|
|
|
|
200,
|
|
|
|
|
|
scrollable: find.byType(Scrollable).first,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.enterText(
|
|
|
|
|
|
find.widgetWithText(TextField, 'Server address'),
|
|
|
|
|
|
'nothing.local',
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(
|
|
|
|
|
|
find.text('No server answered at that address over HTTPS.'),
|
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.enterText(
|
|
|
|
|
|
find.widgetWithText(TextField, 'Server address'),
|
|
|
|
|
|
'http://jellyfin.local:8920',
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.tap(find.text('Continue'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(
|
|
|
|
|
|
find.text(
|
|
|
|
|
|
"That address isn't encrypted, and your password would travel in "
|
|
|
|
|
|
"the clear. Use the server's HTTPS address.",
|
|
|
|
|
|
),
|
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await findServer(tester);
|
|
|
|
|
|
expect(find.text('Living room'), findsOneWidget);
|
|
|
|
|
|
await tester.enterText(find.widgetWithText(TextField, 'User name'), 'mika');
|
|
|
|
|
|
await tester.enterText(find.widgetWithText(TextField, 'Password'), 'wrong');
|
|
|
|
|
|
await tester.tap(find.text('Sign in'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.text('The user name or password is wrong.'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.enterText(
|
|
|
|
|
|
find.widgetWithText(TextField, 'Password'),
|
|
|
|
|
|
'secret',
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.tap(find.text('Sign in'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('mika on Living room'), findsOneWidget);
|
|
|
|
|
|
expect(store.kept?.account?.token, 'token-1');
|
|
|
|
|
|
await tester.tap(find.byTooltip('Sign out of Jellyfin'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.widgetWithText(TextField, 'Server address'), findsOneWidget);
|
|
|
|
|
|
expect(jellyfin.signedOut, ['token-1']);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('a code approved in another Jellyfin app signs in', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
await show(tester, RoutePaths.jellyfin('japanese'));
|
|
|
|
|
|
await findServer(tester);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Sign in with a code'));
|
|
|
|
|
|
// It waits, spinning, until the code is approved.
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
expect(find.text('123456'), findsOneWidget);
|
|
|
|
|
|
await tester.pump(const Duration(seconds: 3));
|
|
|
|
|
|
expect(find.text('123456'), findsOneWidget);
|
|
|
|
|
|
jellyfin.approved = true;
|
|
|
|
|
|
await tester.pump(const Duration(seconds: 3));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Anime'), findsOneWidget);
|
|
|
|
|
|
expect(store.kept?.account?.token, 'token-2');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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 music library lists its albums and songs; a song without '
|
|
|
|
|
|
'lyrics is greyed and opens nothing, one with lyrics plays', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
jellyfin = sampleMusic();
|
|
|
|
|
|
store = signedInJellyfin();
|
|
|
|
|
|
await show(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
RoutePaths.jellyfin(
|
|
|
|
|
|
'japanese',
|
|
|
|
|
|
parentId: 'album-1',
|
|
|
|
|
|
name: 'Sample Album',
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Sample Song'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('No lyrics'), findsOneWidget);
|
|
|
|
|
|
await tester.tap(find.text('Sample Song (Instrumental)'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.text('Sample Song (Instrumental)'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Sample Song'));
|
|
|
|
|
|
for (var frame = 0; frame < 6; frame++) {
|
|
|
|
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
|
|
|
|
}
|
|
|
|
|
|
expect(find.text('Words in this song'), 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('a show tied to the studyset comes first next time; an '
|
|
|
|
|
|
'episode opens to be watched', (tester) async {
|
|
|
|
|
|
store = signedInJellyfin();
|
|
|
|
|
|
await show(tester, RoutePaths.jellyfin('japanese'));
|
|
|
|
|
|
expect(find.text('Tied to this studyset'), findsNothing);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Anime'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
await tester.tap(find.byTooltip('Tie to this studyset'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.byTooltip('Untie from this studyset'), findsOneWidget);
|
|
|
|
|
|
await tester.pageBack();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Tied to this studyset'), findsOneWidget);
|
|
|
|
|
|
await tester.tap(find.text('Sample Journey'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
await tester.tap(find.text('Season 1'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.text('S1 E1 · 24 min'), findsOneWidget);
|
|
|
|
|
|
await tester.tap(find.text('The Station'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.byType(WatchScreen), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('a search finds shows and episodes by their name', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
store = signedInJellyfin();
|
|
|
|
|
|
await show(tester, RoutePaths.jellyfin('japanese'));
|
|
|
|
|
|
|
|
|
|
|
|
await tester.enterText(find.byType(TextField), 'station');
|
|
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('The Station'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Libraries'), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat(jellyfin): From Jellyfin once signed in, the panel in the glow's colour, adding the words listed
Add material's entry is now From Jellyfin (Aus Jellyfin), with a play
icon rather than a TV, as it plays music too, and shows only once the
device is signed in to a server in Settings, as the assistant shows only
once switched on; Settings says where it then is. The screen it opens is
called Jellyfin, and its search finds music too.
The words' panel takes the glow's colour as the scene changes: each copy
the glow takes (every 3 s) gives its average, no lighter than 35 % so the
text stays readable, as strong as the glow and fading into the next as
it does. In the light theme, which has no glow, the panel is tinted from
it while the pointer is over it. What lights up in the panel takes a
scheme made from the same colour; the episode's thumbnail is no longer
used.
While the studyset holds none of the video's words, the study button
adds the words listed (Add 11 words, as the filter shows them), tagged
as a word added alone is; afterwards it studies them, and its menu adds
the new ones listed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:03:37 +02:00
|
|
|
|
testWidgets("the studyset's Add material offers Jellyfin only once this "
|
|
|
|
|
|
'device is signed in to a server (in Settings), as the assistant '
|
|
|
|
|
|
'shows only once switched on', (tester) async {
|
|
|
|
|
|
await show(tester, RoutePaths.studyset('japanese'));
|
|
|
|
|
|
await tester.tap(find.text('Add material'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Create a path'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('From Jellyfin'), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('signed in, Add material › From Jellyfin opens its '
|
|
|
|
|
|
'libraries', (tester) async {
|
|
|
|
|
|
store = signedInJellyfin();
|
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 show(tester, RoutePaths.studyset('japanese'));
|
|
|
|
|
|
await tester.tap(find.text('Add material'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
feat(jellyfin): From Jellyfin once signed in, the panel in the glow's colour, adding the words listed
Add material's entry is now From Jellyfin (Aus Jellyfin), with a play
icon rather than a TV, as it plays music too, and shows only once the
device is signed in to a server in Settings, as the assistant shows only
once switched on; Settings says where it then is. The screen it opens is
called Jellyfin, and its search finds music too.
The words' panel takes the glow's colour as the scene changes: each copy
the glow takes (every 3 s) gives its average, no lighter than 35 % so the
text stays readable, as strong as the glow and fading into the next as
it does. In the light theme, which has no glow, the panel is tinted from
it while the pointer is over it. What lights up in the panel takes a
scheme made from the same colour; the episode's thumbnail is no longer
used.
While the studyset holds none of the video's words, the study button
adds the words listed (Add 11 words, as the filter shows them), tagged
as a word added alone is; afterwards it studies them, and its menu adds
the new ones listed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:03:37 +02:00
|
|
|
|
await tester.tap(find.text('From Jellyfin'));
|
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.pump();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
feat(jellyfin): From Jellyfin once signed in, the panel in the glow's colour, adding the words listed
Add material's entry is now From Jellyfin (Aus Jellyfin), with a play
icon rather than a TV, as it plays music too, and shows only once the
device is signed in to a server in Settings, as the assistant shows only
once switched on; Settings says where it then is. The screen it opens is
called Jellyfin, and its search finds music too.
The words' panel takes the glow's colour as the scene changes: each copy
the glow takes (every 3 s) gives its average, no lighter than 35 % so the
text stays readable, as strong as the glow and fading into the next as
it does. In the light theme, which has no glow, the panel is tinted from
it while the pointer is over it. What lights up in the panel takes a
scheme made from the same colour; the episode's thumbnail is no longer
used.
While the studyset holds none of the video's words, the study button
adds the words listed (Add 11 words, as the filter shows them), tagged
as a word added alone is; afterwards it studies them, and its menu adds
the new ones listed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:03:37 +02:00
|
|
|
|
expect(find.text('Libraries'), 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
|
|
|
|
});
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
|
|
|
|
|
|
group('a Jellyfin server the instance suggests (decided 2026-10-13)', () {
|
|
|
|
|
|
setUp(() {
|
2026-10-14 01:52:42 +02:00
|
|
|
|
jellyfin
|
|
|
|
|
|
..shownUsers = const [
|
|
|
|
|
|
JellyfinPublicUser(id: 'user-2', name: 'Demo'),
|
|
|
|
|
|
JellyfinPublicUser(id: 'user-1', name: 'mika'),
|
|
|
|
|
|
]
|
|
|
|
|
|
..withoutPassword = const {'Demo'};
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('Add material offers it before signing in; the sign-in '
|
|
|
|
|
|
"finds it by itself and lists the server's public people, one "
|
2026-10-14 01:52:42 +02:00
|
|
|
|
'without a password signed in with the password left empty', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
await show(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
RoutePaths.studyset('japanese'),
|
|
|
|
|
|
suggested: 'https://jellyfin.local:8920',
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.tap(find.text('Add material'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
await tester.tap(find.text('From Jellyfin'));
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Living room'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Demo'), findsOneWidget);
|
|
|
|
|
|
await tester.tap(find.text('Demo'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
2026-10-14 01:52:42 +02:00
|
|
|
|
expect(store.kept?.account, isNull);
|
|
|
|
|
|
await tester.tap(find.text('Sign in'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
|
|
|
|
|
|
expect(find.text('Libraries'), findsOneWidget);
|
|
|
|
|
|
expect(store.kept?.account?.userName, 'Demo');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-14 01:52:42 +02:00
|
|
|
|
testWidgets('a tap names one in the form, which asks for the '
|
|
|
|
|
|
'password', (tester) async {
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
await show(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
RoutePaths.jellyfin('japanese'),
|
|
|
|
|
|
suggested: 'https://jellyfin.local:8920',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('mika'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
|
tester
|
|
|
|
|
|
.widget<TextField>(find.widgetWithText(TextField, 'User name'))
|
|
|
|
|
|
.controller!
|
|
|
|
|
|
.text,
|
|
|
|
|
|
'mika',
|
|
|
|
|
|
);
|
2026-10-14 01:52:42 +02:00
|
|
|
|
expect(
|
|
|
|
|
|
tester
|
|
|
|
|
|
.widget<TextField>(find.widgetWithText(TextField, 'Password'))
|
|
|
|
|
|
.focusNode!
|
|
|
|
|
|
.hasFocus,
|
|
|
|
|
|
isTrue,
|
|
|
|
|
|
);
|
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.
In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 23:25:33 +02:00
|
|
|
|
expect(store.kept?.account, isNull);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
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
|
|
|
|
}
|