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';
|
feat(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 21:55:00 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_dictionaries.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/dictionary/data/dictionary_adder.dart';
|
feat(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 21:55:00 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/device_dictionary.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/dictionary/model/dictionary.dart';
|
feat(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 21:55:00 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/sentence_words.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';
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.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';
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.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/model/studyset_content.dart';
|
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
feat(jellyfin): a show's tags follow it whatever it is called
A word met on Jellyfin is tagged with the show and the episode by the
names Jellyfin gives them, so a server switching its metadata language,
another server, or a rename in Manage tags made a second set of tags for
the same show. Such a tag now also keeps what it stands for: the ids the
show (or the artist, or the film) has with the metadata providers and on
the server, and the episode's number or the song's own id. New words
find their tag by those first, keeping its name; a Jellyfin tag from
before, found by its name, takes them, and a rename keeps them.
They stay in tag schema 2, a deliberate exception to ADR 0002's rule 3:
schema 3 would hide every new Jellyfin tag from v0.17, while an app
that rewrites the tag only drops the ids, and the tag is found by its
name again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 22:34:28 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.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:material_ui/material_ui.dart';
|
|
|
|
|
|
|
|
|
|
|
|
import '../../support/fake_card_gateway.dart';
|
feat(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 21:55:00 +02:00
|
|
|
|
import '../../support/fake_device_dictionaries.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';
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
import '../../support/fake_settings_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;
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
late FakeStudysetGateway studysets;
|
|
|
|
|
|
late FakeSettingsGateway settingsGateway;
|
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
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
studysets = FakeStudysetGateway()
|
|
|
|
|
|
..receiveRemoteChange(
|
|
|
|
|
|
'japanese',
|
|
|
|
|
|
encodeStudysetPayload(
|
|
|
|
|
|
StudysetContent(
|
|
|
|
|
|
title: 'Japanese',
|
|
|
|
|
|
description: RichContent.empty(),
|
|
|
|
|
|
frontLanguage: 'ja',
|
|
|
|
|
|
backLanguage: 'en',
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
settingsGateway = FakeSettingsGateway();
|
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
|
|
|
|
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(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 21:55:00 +02:00
|
|
|
|
FakeDictionaryGateway? dictionary,
|
|
|
|
|
|
DeviceDictionaries? device,
|
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);
|
|
|
|
|
|
if (!playerAvailable) {
|
|
|
|
|
|
players = FakeMediaPlayers(available: false);
|
|
|
|
|
|
}
|
|
|
|
|
|
await pumpApp(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
testDependencies(
|
|
|
|
|
|
studysetGateway: studysets,
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
settingsGateway: settingsGateway,
|
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
|
|
|
|
cardGateway: cardGateway,
|
|
|
|
|
|
tagGateway: tagGateway,
|
feat(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 21:55:00 +02:00
|
|
|
|
dictionaryGateway:
|
|
|
|
|
|
dictionary ??
|
|
|
|
|
|
(words == null
|
|
|
|
|
|
? sampleDictionary()
|
|
|
|
|
|
: FakeDictionaryGateway(words: words)),
|
|
|
|
|
|
deviceDictionaries: device,
|
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
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
/// Adds words as the screen does, into [studysets].
|
|
|
|
|
|
DictionaryAdder adder() => DictionaryAdder(
|
|
|
|
|
|
cards,
|
|
|
|
|
|
tags,
|
|
|
|
|
|
StudysetRepository(studysets),
|
|
|
|
|
|
SettingsRepository(settingsGateway),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
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
|
|
|
|
/// 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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat(dictionary): Japanese cut into words and looked up on the device
Once the person agrees, the device holds MeCab's IPADIC and the
instance's JMdict and cuts lines as Jisho.org does, so their text never
leaves it (ADR 0012). MeCab comes through mecab_for_dart, whose build
hook compiles it; its tokens are grouped by Ve's rules, written anew in
Dart, with に kept a word of its own. Each word is found as it stands or
as a form of a word, else by its dictionary form, else as the longest
words it holds from token to token; longer words over token boundaries
are offered as before.
The curator packs Ubuntu's compiled IPADIC beside the dictionary files
(curate ipadic, and every update run). The server exports its JMdict
from its tables, so the device finds the same keys and keeps the same
versions, and serves both at /device-dictionaries/, named in
dictionary.deviceFiles('ja') with their hashes; Caddy forwards the path.
The app checks each file against its hash, unpacks IPADIC, builds a
SQLite database from the export on an isolate, and asks the instance
for newer files every 30 days, downloading only what changed. The Watch
screen offers the download once per device with its size (34 MB, some
150 MB once set up); Settings downloads and removes them. Without them,
or should cutting on the device fail, lines go to the instance as
before. MeCab's, IPADIC's and Ve's licences join the licence list.
Every build now needs a C++ compiler; mecab_for_dart holds code_assets
at 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 21:55:00 +02:00
|
|
|
|
testWidgets('offers once to look words up on the device, saying how much '
|
|
|
|
|
|
'it downloads; Not now offers it no more', (tester) async {
|
|
|
|
|
|
final device = deviceDictionariesIn(
|
|
|
|
|
|
FakeDeviceDictionaryGateway(offered: offeredJapanese()),
|
|
|
|
|
|
);
|
|
|
|
|
|
await watch(tester, device: device);
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Look words up on this device?'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('(34.0 MB, more once set up)'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Not now'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('Look words up on this device?'), findsNothing);
|
|
|
|
|
|
expect(device.status.asked, isTrue);
|
|
|
|
|
|
expect(device.status.progress, isNull);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('with Japanese’s dictionaries on the device, lines are cut '
|
|
|
|
|
|
'there, and the instance never sees them', (tester) async {
|
|
|
|
|
|
final analyser = FakeJapaneseAnalyser(
|
|
|
|
|
|
cut: (sentence) => [
|
|
|
|
|
|
SentencePart(text: sentence, start: 0, entries: [sampleWords.first]),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
final dictionary = sampleDictionary();
|
|
|
|
|
|
await watch(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
dictionary: dictionary,
|
|
|
|
|
|
device: deviceDictionariesIn(
|
|
|
|
|
|
FakeDeviceDictionaryGateway(offered: offeredJapanese()),
|
|
|
|
|
|
analyser: analyser,
|
|
|
|
|
|
initial: DeviceDictionaryStatus(
|
|
|
|
|
|
installed: installedJapanese(),
|
|
|
|
|
|
asked: true,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
|
analyser.asked.expand((sentences) => sentences),
|
|
|
|
|
|
contains('昨日、駅で友達に会いました。'),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(dictionary.keysAsked, isEmpty);
|
|
|
|
|
|
expect(find.text('Look words up on this device?'), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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 word tapped pauses the video and shows what it is a form '
|
feat(tag): one switch hides the tags from Jellyfin while studying
Every tag the app makes for where words were met on Jellyfin (the show
and the episode, the artist and the song) now says so (`origin` in tag
schema 2, written only for such a tag), in place of a tag hidden one by
one. Manage tags, where there are such tags, has one switch, Hide tags
from Jellyfin while studying (`hideJellyfinTags`, settings schema 9,
following the person): with it on, cards being studied leave them out of
their row at once, and Tags and Manage tags list them faint. They still
filter and narrow a session. The per-tag eye button is gone.
A word added while watching no longer takes the line it was met in as
its first hint: its hints are the dictionary's examples alone. Tatoeba's
sentences are checked, a subtitle line may be wrong or mistyped, a word
met again in a better line wouldn't get it, and every episode's line
would pile up on a common word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 20:52:22 +02:00
|
|
|
|
"of; added, it is a flashcard with the dictionary's checked examples "
|
|
|
|
|
|
'(never its line, which may be wrong), tagged with the show and the '
|
|
|
|
|
|
'episode as tags from Jellyfin, and is marked from then on', (
|
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,
|
|
|
|
|
|
) 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();
|
|
|
|
|
|
|
feat(tag): one switch hides the tags from Jellyfin while studying
Every tag the app makes for where words were met on Jellyfin (the show
and the episode, the artist and the song) now says so (`origin` in tag
schema 2, written only for such a tag), in place of a tag hidden one by
one. Manage tags, where there are such tags, has one switch, Hide tags
from Jellyfin while studying (`hideJellyfinTags`, settings schema 9,
following the person): with it on, cards being studied leave them out of
their row at once, and Tags and Manage tags list them faint. They still
filter and narrow a session. The per-tag eye button is gone.
A word added while watching no longer takes the line it was met in as
its first hint: its hints are the dictionary's examples alone. Tatoeba's
sentences are checked, a subtitle line may be wrong or mistyped, a word
met again in a better line wouldn't get it, and every episode's line
would pile up on a common word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 20:52:22 +02:00
|
|
|
|
expect(find.text('会う added'), 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
|
|
|
|
final card = (await cards.cardsOf('japanese')).single;
|
|
|
|
|
|
expect(card.content.type, CardType.vocabulary);
|
|
|
|
|
|
expect(card.content.entry, (source: 'jmdict', id: '6'));
|
feat(tag): one switch hides the tags from Jellyfin while studying
Every tag the app makes for where words were met on Jellyfin (the show
and the episode, the artist and the song) now says so (`origin` in tag
schema 2, written only for such a tag), in place of a tag hidden one by
one. Manage tags, where there are such tags, has one switch, Hide tags
from Jellyfin while studying (`hideJellyfinTags`, settings schema 9,
following the person): with it on, cards being studied leave them out of
their row at once, and Tags and Manage tags list them faint. They still
filter and narrow a session. The per-tag eye button is gone.
A word added while watching no longer takes the line it was met in as
its first hint: its hints are the dictionary's examples alone. Tatoeba's
sentences are checked, a subtitle line may be wrong or mistyped, a word
met again in a better line wouldn't get it, and every episode's line
would pile up on a common word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 20:52:22 +02:00
|
|
|
|
expect(card.content.hints, hasLength(1));
|
|
|
|
|
|
expect(card.content.hints.single.plainText, contains('また会いましょう。'));
|
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
|
|
|
|
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
|
|
|
|
);
|
feat(tag): one switch hides the tags from Jellyfin while studying
Every tag the app makes for where words were met on Jellyfin (the show
and the episode, the artist and the song) now says so (`origin` in tag
schema 2, written only for such a tag), in place of a tag hidden one by
one. Manage tags, where there are such tags, has one switch, Hide tags
from Jellyfin while studying (`hideJellyfinTags`, settings schema 9,
following the person): with it on, cards being studied leave them out of
their row at once, and Tags and Manage tags list them faint. They still
filter and narrow a session. The per-tag eye button is gone.
A word added while watching no longer takes the line it was met in as
its first hint: its hints are the dictionary's examples alone. Tatoeba's
sentences are checked, a subtitle line may be wrong or mistyped, a word
met again in a better line wouldn't get it, and every episode's line
would pile up on a common word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 20:52:22 +02:00
|
|
|
|
expect(tagging.named('Sample Journey S1 E1')?.fromJellyfin, isTrue);
|
|
|
|
|
|
expect(tagging.named('Sample Journey')?.fromJellyfin, isTrue);
|
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
|
|
|
|
});
|
|
|
|
|
|
|
feat(jellyfin): a show's tags follow it whatever it is called
A word met on Jellyfin is tagged with the show and the episode by the
names Jellyfin gives them, so a server switching its metadata language,
another server, or a rename in Manage tags made a second set of tags for
the same show. Such a tag now also keeps what it stands for: the ids the
show (or the artist, or the film) has with the metadata providers and on
the server, and the episode's number or the song's own id. New words
find their tag by those first, keeping its name; a Jellyfin tag from
before, found by its name, takes them, and a rename keeps them.
They stay in tag schema 2, a deliberate exception to ADR 0002's rule 3:
schema 3 would hide every new Jellyfin tag from v0.17, while an app
that rewrites the tag only drops the ids, and the tag is found by its
name again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 22:34:28 +02:00
|
|
|
|
testWidgets('a show the server calls otherwise now keeps its tags: a word '
|
|
|
|
|
|
'added gets those standing for the show and the episode, whatever '
|
|
|
|
|
|
'they are called', (tester) async {
|
|
|
|
|
|
final show = await tags.create(
|
|
|
|
|
|
'旅の記録',
|
|
|
|
|
|
fromJellyfin: true,
|
|
|
|
|
|
jellyfinId: const JellyfinTagId(ids: {'tmdb': '424242'}),
|
|
|
|
|
|
);
|
|
|
|
|
|
final episode = await tags.create(
|
|
|
|
|
|
'旅の記録 1-1',
|
|
|
|
|
|
fromJellyfin: true,
|
|
|
|
|
|
jellyfinId: const JellyfinTagId(ids: {'tmdb': '424242'}, part: 'S1E1'),
|
|
|
|
|
|
);
|
|
|
|
|
|
await watch(tester);
|
|
|
|
|
|
await players.last!.play();
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('いました'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
await tester.tap(find.text('Add'));
|
|
|
|
|
|
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.id],
|
|
|
|
|
|
containsAll([show, episode]),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(tagging.named('Sample Journey'), isNull);
|
|
|
|
|
|
expect(tagging.named('Sample Journey S1 E1'), 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
|
|
|
|
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('で')));
|
|
|
|
|
|
|
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
|
|
|
|
// Further down the list.
|
|
|
|
|
|
final wait = find.textContaining('待つ', findRichText: true);
|
|
|
|
|
|
await tester.scrollUntilVisible(
|
|
|
|
|
|
wait,
|
|
|
|
|
|
100,
|
|
|
|
|
|
scrollable: find
|
|
|
|
|
|
.descendant(
|
|
|
|
|
|
of: find.byType(CustomScrollView),
|
|
|
|
|
|
matching: find.byType(Scrollable),
|
|
|
|
|
|
)
|
|
|
|
|
|
.first,
|
|
|
|
|
|
);
|
2026-10-13 19:32:37 +02:00
|
|
|
|
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(wait.first);
|
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 '
|
feat: a round in the order cards come, shared daily limits, and a start screen
Studying:
- A round is one list in the order its cards come: those done, the one
shown, those to come. A card due again within the day goes back in
after 4 more cards when rated Bad, at the end otherwise, so the pills
above the card follow it; the buttons say "in 4 cards", "at the end",
"now" (round_order.dart).
- The studied studyset's daily limits hold for everything inside it
together, as Anki's do for subdecks: twenty topics start 20 new cards
a day in all, not 20 each.
- The study header's studyset name shows whole on hover or long press.
- The assistant's button sits at the lower left: in the Library across
from Create studyset, in a studyset above its bar, and higher while
studying, above the buttons that answer the card.
A studyset:
- Progress also counts the learning paths' steps done and lists the
PDFs not read to their end, begun first, each opening the reader,
across its topics (MaterialProgressCubit).
- Next up starts with studying for the exam while one is ahead: its
reviews due and the new cards its pace leaves for the day.
- Learning paths tagged with a level count towards its automatic exam
plan and offer it, as cards do.
- A step's first heading equal to its title is dropped on import and
hidden in paths imported before.
Signing out, or a session the server ended, leads to a start screen
(GitHub, email, without an account, another instance) rather than a new
anonymous account; only the very first start makes one by itself.
Settings names the kind of account and its instance.
Also: the Watch screen reads the cards once for held words and their
progress; its ±10 s buttons tick as the navigation bar does; a map card
keeps its view as its caption changes and brings a picked region
towards the middle; an empty or failed library can be pulled to sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-17 00:39:34 +02:00
|
|
|
|
'beside it, each a short tick where the device vibrates, and where it '
|
|
|
|
|
|
'is from 0:00 to its length', (tester) async {
|
|
|
|
|
|
final ticks = <Object?>[];
|
|
|
|
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
|
|
|
|
SystemChannels.platform,
|
|
|
|
|
|
(call) async {
|
|
|
|
|
|
if (call.method == 'HapticFeedback.vibrate') {
|
|
|
|
|
|
ticks.add(call.arguments);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
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 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));
|
feat: a round in the order cards come, shared daily limits, and a start screen
Studying:
- A round is one list in the order its cards come: those done, the one
shown, those to come. A card due again within the day goes back in
after 4 more cards when rated Bad, at the end otherwise, so the pills
above the card follow it; the buttons say "in 4 cards", "at the end",
"now" (round_order.dart).
- The studied studyset's daily limits hold for everything inside it
together, as Anki's do for subdecks: twenty topics start 20 new cards
a day in all, not 20 each.
- The study header's studyset name shows whole on hover or long press.
- The assistant's button sits at the lower left: in the Library across
from Create studyset, in a studyset above its bar, and higher while
studying, above the buttons that answer the card.
A studyset:
- Progress also counts the learning paths' steps done and lists the
PDFs not read to their end, begun first, each opening the reader,
across its topics (MaterialProgressCubit).
- Next up starts with studying for the exam while one is ahead: its
reviews due and the new cards its pace leaves for the day.
- Learning paths tagged with a level count towards its automatic exam
plan and offer it, as cards do.
- A step's first heading equal to its title is dropped on import and
hidden in paths imported before.
Signing out, or a session the server ended, leads to a start screen
(GitHub, email, without an account, another instance) rather than a new
anonymous account; only the very first start makes one by itself.
Settings names the kind of account and its instance.
Also: the Watch screen reads the cards once for held words and their
progress; its ±10 s buttons tick as the navigation bar does; a map card
keeps its view as its caption changes and brings a picked region
towards the middle; an empty or failed library can be pulled to sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-17 00:39:34 +02:00
|
|
|
|
expect(ticks, List.filled(3, 'HapticFeedbackType.selectionClick'));
|
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("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 == '駅');
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
await adder().add('japanese', [station]);
|
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
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
testWidgets("watching from a studyset with topics, a topic's word shows "
|
|
|
|
|
|
'as held, and a new one goes where Adds to says, shown as added at '
|
|
|
|
|
|
'once', (tester) async {
|
|
|
|
|
|
studysets.receiveRemoteChange(
|
|
|
|
|
|
'n3',
|
|
|
|
|
|
encodeStudysetPayload(StudysetContent.titled('N3')),
|
|
|
|
|
|
parentId: 'japanese',
|
|
|
|
|
|
);
|
|
|
|
|
|
final station = sampleWords.firstWhere((word) => word.headword == '駅');
|
|
|
|
|
|
await adder().add('n3', [station]);
|
|
|
|
|
|
await watch(tester);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
// The filter's menu stays open for more choices.
|
|
|
|
|
|
await tester.tap(find.byTooltip('Filter the words'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Adds to Japanese'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
// The words' levels say N3 too.
|
|
|
|
|
|
await tester.tap(find.widgetWithText(MenuItemButton, 'N3'));
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.text('Adds to N3'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
await players.last!.play();
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
|
await tester.tap(find.text('いました'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
await tester.tap(find.text('Add'));
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
// Held and new at once, before the card is written.
|
|
|
|
|
|
expect(find.text('Add'), findsNothing);
|
|
|
|
|
|
expect(find.text('New'), findsOneWidget);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('会う added'), findsOneWidget);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
[for (final card in await cards.cardsOf('n3')) card.content.entry?.id],
|
|
|
|
|
|
containsAll(['6']),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(await cards.cardsOf('japanese'), isEmpty);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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('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);
|
2026-10-17 14:23:47 +02:00
|
|
|
|
// Its first line: the card scrolls, and its middle may lie beyond it.
|
|
|
|
|
|
await tester.tap(
|
|
|
|
|
|
find.descendant(
|
|
|
|
|
|
of: find.byType(VocabularySensesView),
|
|
|
|
|
|
matching: find.textContaining('sense 1 of a long'),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
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), 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 22:38:29 +02:00
|
|
|
|
group('a stretch chosen by hand (decided 2026-10-13: by characters)', () {
|
|
|
|
|
|
final greetings = [
|
|
|
|
|
|
word('40', '今日', reading: 'きょう', pos: ['n'], glosses: ['today']),
|
|
|
|
|
|
word('41', '今日は', reading: 'こんにちは', pos: ['int'], glosses: ['hello']),
|
|
|
|
|
|
word('42', 'は', pos: ['prt'], labels: ['common']),
|
|
|
|
|
|
word('43', '雨', reading: 'あめ', pos: ['n'], glosses: ['rain']),
|
|
|
|
|
|
word('44', 'だ', pos: ['cop'], labels: ['common']),
|
|
|
|
|
|
];
|
|
|
|
|
|
Future<void> watchGreetings(WidgetTester tester) async {
|
|
|
|
|
|
await watch(
|
|
|
|
|
|
tester,
|
|
|
|
|
|
vtt: {
|
|
|
|
|
|
('episode-1', 2): 'WEBVTT\n\n00:00:01.000 --> 00:00:05.000\n今日は雨だ\n',
|
|
|
|
|
|
('episode-1', 3):
|
|
|
|
|
|
'WEBVTT\n\n00:00:01.000 --> 00:00:05.000\nA grey sky.\n',
|
|
|
|
|
|
},
|
|
|
|
|
|
words: greetings,
|
|
|
|
|
|
);
|
|
|
|
|
|
await playTo(tester, const Duration(seconds: 2));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The middle of the [index]th character of the base text [text] of
|
|
|
|
|
|
/// the line (four characters to a run at most here, each as wide).
|
|
|
|
|
|
Offset character(WidgetTester tester, String text, int index) {
|
|
|
|
|
|
final rect = tester.getRect(find.text(text, findRichText: true).first);
|
|
|
|
|
|
final width = rect.width / text.length;
|
|
|
|
|
|
return Offset(rect.left + width * (index + 0.5), rect.center.dy);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('dragged across with a mouse, the characters chosen are '
|
|
|
|
|
|
'looked up as one word, also inside a word the cut took longer: '
|
|
|
|
|
|
'今日 out of 今日は', (tester) async {
|
|
|
|
|
|
await watchGreetings(tester);
|
|
|
|
|
|
await players.last!.play();
|
|
|
|
|
|
expect(find.text('今日', findRichText: true), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
final drag = await tester.startGesture(
|
|
|
|
|
|
character(tester, '今日', 0),
|
|
|
|
|
|
kind: PointerDeviceKind.mouse,
|
|
|
|
|
|
);
|
|
|
|
|
|
await drag.moveTo(character(tester, '今日', 1));
|
|
|
|
|
|
await drag.up();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(players.last!.state.value.playing, isFalse);
|
|
|
|
|
|
expect(find.textContaining('today', findRichText: true), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('hello', findRichText: true), findsNothing);
|
|
|
|
|
|
// While chosen, the line reads it as the word chosen: きょう, not
|
|
|
|
|
|
// こんにち of こんにちは.
|
|
|
|
|
|
expect(find.text('こんにち'), findsNothing);
|
|
|
|
|
|
expect(find.text('きょう'), findsWidgets);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('Shift+click chooses from the word chosen to the character '
|
|
|
|
|
|
'clicked; what the dictionary lacks as one word is searched on '
|
|
|
|
|
|
'Jisho', (tester) async {
|
|
|
|
|
|
await watchGreetings(tester);
|
|
|
|
|
|
await tester.tap(find.text('雨', findRichText: true));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
expect(find.textContaining('rain', findRichText: true), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
|
|
|
|
final click = await tester.startGesture(
|
|
|
|
|
|
character(tester, 'だ', 0),
|
|
|
|
|
|
kind: PointerDeviceKind.mouse,
|
|
|
|
|
|
);
|
|
|
|
|
|
await click.up();
|
|
|
|
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
|
find.text("雨だ isn't in the dictionary as one word."),
|
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(find.text('Search Jisho for it'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('rain', findRichText: true), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('on a phone, holding a character starts a stretch, and '
|
|
|
|
|
|
'moving on extends it', (tester) async {
|
|
|
|
|
|
await watchGreetings(tester);
|
|
|
|
|
|
|
|
|
|
|
|
final hold = await tester.startGesture(character(tester, '今日', 0));
|
|
|
|
|
|
await tester.pump(const Duration(milliseconds: 600));
|
|
|
|
|
|
await hold.moveTo(character(tester, '今日', 1));
|
|
|
|
|
|
await hold.up();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.textContaining('today', findRichText: true), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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 == '駅');
|
fix(dictionary): words held in topics, a chosen destination, faster adds
A word counted as held only in the studyset itself, so adding from a
studyset whose topics held it (Vocabulary, its words in N3) made a
second card, and watching from a top-level studyset put every new word
into it. Now a word is held anywhere in the studyset and its topics,
archived ones too, and its progress shows from that card; new words go
where the person chose for the studyset ("Adds to N3" in the dictionary
and the Watch screen, the studyset and its topics but archived ones,
kept in their settings, schema 10), the studyset itself until then.
Adding a word took about two seconds on 17,000 card tags: each of its
four tags read every card's tags twice. They are now written with one
read, the Watch screen shows the word added at once and takes it back
should writing fail, and it no longer reads the studyset's progress
again after each word. On a copy of a real database an Add takes 0.3 to
0.4 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-16 20:41:32 +02:00
|
|
|
|
await adder().add('japanese', [station]);
|
2026-10-13 19:32:37 +02:00
|
|
|
|
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();
|
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
|
|
|
|
// The words listed it lacks can be added from here too.
|
|
|
|
|
|
expect(
|
|
|
|
|
|
find.textContaining(RegExp(r'^Add the \d+ new words listed$')),
|
|
|
|
|
|
findsOneWidget,
|
|
|
|
|
|
);
|
2026-10-13 19:32:37 +02:00
|
|
|
|
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): 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("with none of the video's words held, the button adds the "
|
|
|
|
|
|
'words listed, tagged from Jellyfin, and then studies them', (
|
|
|
|
|
|
tester,
|
|
|
|
|
|
) async {
|
|
|
|
|
|
await watch(tester);
|
|
|
|
|
|
|
|
|
|
|
|
final add = find.textContaining(RegExp(r'^Add \d+ words$'));
|
|
|
|
|
|
expect(add, findsOneWidget);
|
|
|
|
|
|
final listed = int.parse(
|
|
|
|
|
|
RegExp(r'\d+').firstMatch(tester.widget<Text>(add).data!)![0]!,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(listed, greaterThan(1));
|
|
|
|
|
|
await tester.tap(add);
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.text('$listed words added'), findsOneWidget);
|
|
|
|
|
|
final added = await cards.cardsOf('japanese');
|
|
|
|
|
|
expect(added, hasLength(listed));
|
|
|
|
|
|
final tagging = await tags.read();
|
|
|
|
|
|
for (final card in added) {
|
|
|
|
|
|
expect(
|
|
|
|
|
|
[for (final tag in tagging.of(card.id)) tag.name],
|
|
|
|
|
|
containsAll(['Sample Journey', 'Sample Journey S1 E1']),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
expect(find.text('Study'), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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']),
|
|
|
|
|
|
);
|
feat(tag): one switch hides the tags from Jellyfin while studying
Every tag the app makes for where words were met on Jellyfin (the show
and the episode, the artist and the song) now says so (`origin` in tag
schema 2, written only for such a tag), in place of a tag hidden one by
one. Manage tags, where there are such tags, has one switch, Hide tags
from Jellyfin while studying (`hideJellyfinTags`, settings schema 9,
following the person): with it on, cards being studied leave them out of
their row at once, and Tags and Manage tags list them faint. They still
filter and narrow a session. The per-tag eye button is gone.
A word added while watching no longer takes the line it was met in as
its first hint: its hints are the dictionary's examples alone. Tatoeba's
sentences are checked, a subtitle line may be wrong or mistyped, a word
met again in a better line wouldn't get it, and every episode's line
would pile up on a common word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 20:52:22 +02:00
|
|
|
|
expect(
|
|
|
|
|
|
tagging.named('Sample Artist · Sample Song')?.fromJellyfin,
|
|
|
|
|
|
isTrue,
|
|
|
|
|
|
);
|
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
|
|
|
|
});
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|