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>
This commit is contained in:
parent
436dc884b6
commit
22f5a4fcd1
1 changed files with 720 additions and 62 deletions
|
|
@ -60,7 +60,12 @@ void main() {
|
|||
List<MediaAudioTrack> audio = const [],
|
||||
List<DictionaryWord>? words,
|
||||
FakeReviewGateway? reviews,
|
||||
FakeJellyfinGateway? gateway,
|
||||
String itemId = 'episode-1',
|
||||
}) async {
|
||||
if (gateway != null) {
|
||||
jellyfin = gateway;
|
||||
}
|
||||
if (playback != null || vtt != null) {
|
||||
jellyfin = sampleJellyfin(
|
||||
playback: playback ?? sampleEpisodePlayback,
|
||||
|
|
@ -104,7 +109,7 @@ void main() {
|
|||
jellyfinStore: signedInJellyfin(),
|
||||
mediaPlayers: players,
|
||||
),
|
||||
location: RoutePaths.watch('japanese', 'episode-1'),
|
||||
location: RoutePaths.watch('japanese', itemId),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
|
@ -627,6 +632,31 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("a song's lyrics move with it, its words looked up as a "
|
||||
"video's; a word added is tagged with the artist and the song", (
|
||||
tester,
|
||||
) async {
|
||||
await watch(tester, gateway: sampleMusic(), itemId: 'song-1');
|
||||
expect(find.text('Words in this song'), findsOneWidget);
|
||||
// Every line shows, cut into words; the empty one is left out.
|
||||
expect(find.text('友達'), findsOneWidget);
|
||||
expect(find.text('読'), findsOneWidget);
|
||||
|
||||
await playTo(tester, const Duration(seconds: 2));
|
||||
await tester.tap(find.text('駅'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Add').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final card = (await cards.cardsOf('japanese')).single;
|
||||
final tagging = await tags.read();
|
||||
expect(
|
||||
[for (final tag in tagging.of(card.id)) tag.name],
|
||||
containsAll(['Sample Artist', 'Sample Artist · Sample Song']),
|
||||
);
|
||||
expect(tagging.named('Sample Artist · Sample Song')?.hidden, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('on a phone the words are below the video', (tester) async {
|
||||
await watch(tester, size: const Size(412, 915));
|
||||
await playTo(tester, const Duration(seconds: 2));
|
||||
|
|
|
|||
Loading…
Reference in a new issue