feat(music): show full play queue with history, YouTube Music style

Queue sheet/panel now renders the whole playback order — dimmed played
tracks above the current one — instead of only up-next. Opens scrolled
to the current track and follows advancement while parked on it.
This commit is contained in:
edde746
2026-07-06 11:30:23 +02:00
parent fcdef343eb
commit 214a7c13dc
36 changed files with 181 additions and 236 deletions
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Сега се възпроизвежда", "nowPlaying": "Сега се възпроизвежда",
"playingFrom": "Възпроизвеждане от ${title}", "playingFrom": "Възпроизвеждане от ${title}",
"queue": "Опашка", "queue": "Опашка",
"upNext": "Следва",
"clearQueue": "Изчисти опашката", "clearQueue": "Изчисти опашката",
"lyrics": "Текст", "lyrics": "Текст",
"noLyrics": "Няма наличен текст", "noLyrics": "Няма наличен текст",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Afspiller nu", "nowPlaying": "Afspiller nu",
"playingFrom": "Afspiller fra ${title}", "playingFrom": "Afspiller fra ${title}",
"queue": "Kø", "queue": "Kø",
"upNext": "Næste i køen",
"clearQueue": "Ryd kø", "clearQueue": "Ryd kø",
"lyrics": "Sangtekst", "lyrics": "Sangtekst",
"noLyrics": "Ingen sangtekst tilgængelig", "noLyrics": "Ingen sangtekst tilgængelig",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Wird wiedergegeben", "nowPlaying": "Wird wiedergegeben",
"playingFrom": "Wiedergabe aus ${title}", "playingFrom": "Wiedergabe aus ${title}",
"queue": "Warteschlange", "queue": "Warteschlange",
"upNext": "Als Nächstes",
"clearQueue": "Warteschlange leeren", "clearQueue": "Warteschlange leeren",
"lyrics": "Songtext", "lyrics": "Songtext",
"noLyrics": "Kein Songtext verfügbar", "noLyrics": "Kein Songtext verfügbar",
-1
View File
@@ -987,7 +987,6 @@
"nowPlaying": "Now Playing", "nowPlaying": "Now Playing",
"playingFrom": "Playing from ${title}", "playingFrom": "Playing from ${title}",
"queue": "Queue", "queue": "Queue",
"upNext": "Up next",
"clearQueue": "Clear queue", "clearQueue": "Clear queue",
"lyrics": "Lyrics", "lyrics": "Lyrics",
"noLyrics": "No lyrics available", "noLyrics": "No lyrics available",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Reproduciendo ahora", "nowPlaying": "Reproduciendo ahora",
"playingFrom": "Reproduciendo desde ${title}", "playingFrom": "Reproduciendo desde ${title}",
"queue": "Cola", "queue": "Cola",
"upNext": "A continuación",
"clearQueue": "Vaciar la cola", "clearQueue": "Vaciar la cola",
"lyrics": "Letra", "lyrics": "Letra",
"noLyrics": "No hay letra disponible", "noLyrics": "No hay letra disponible",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Lecture en cours", "nowPlaying": "Lecture en cours",
"playingFrom": "Lecture depuis ${title}", "playingFrom": "Lecture depuis ${title}",
"queue": "File d'attente", "queue": "File d'attente",
"upNext": "À suivre",
"clearQueue": "Vider la file d'attente", "clearQueue": "Vider la file d'attente",
"lyrics": "Paroles", "lyrics": "Paroles",
"noLyrics": "Aucune parole disponible", "noLyrics": "Aucune parole disponible",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "In riproduzione", "nowPlaying": "In riproduzione",
"playingFrom": "Riproduzione da ${title}", "playingFrom": "Riproduzione da ${title}",
"queue": "Coda", "queue": "Coda",
"upNext": "Da riprodurre",
"clearQueue": "Svuota la coda", "clearQueue": "Svuota la coda",
"lyrics": "Testo", "lyrics": "Testo",
"noLyrics": "Nessun testo disponibile", "noLyrics": "Nessun testo disponibile",
-1
View File
@@ -974,7 +974,6 @@
"nowPlaying": "再生中", "nowPlaying": "再生中",
"playingFrom": "${title} から再生", "playingFrom": "${title} から再生",
"queue": "再生キュー", "queue": "再生キュー",
"upNext": "次はこちら",
"clearQueue": "キューをクリア", "clearQueue": "キューをクリア",
"lyrics": "歌詞", "lyrics": "歌詞",
"noLyrics": "歌詞がありません", "noLyrics": "歌詞がありません",
-1
View File
@@ -974,7 +974,6 @@
"nowPlaying": "재생 중", "nowPlaying": "재생 중",
"playingFrom": "${title}에서 재생 중", "playingFrom": "${title}에서 재생 중",
"queue": "재생 대기열", "queue": "재생 대기열",
"upNext": "다음 항목",
"clearQueue": "대기열 비우기", "clearQueue": "대기열 비우기",
"lyrics": "가사", "lyrics": "가사",
"noLyrics": "가사 없음", "noLyrics": "가사 없음",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Spilles nå", "nowPlaying": "Spilles nå",
"playingFrom": "Spiller fra ${title}", "playingFrom": "Spiller fra ${title}",
"queue": "Kø", "queue": "Kø",
"upNext": "Neste i køen",
"clearQueue": "Tøm kø", "clearQueue": "Tøm kø",
"lyrics": "Sangtekst", "lyrics": "Sangtekst",
"noLyrics": "Ingen sangtekst tilgjengelig", "noLyrics": "Ingen sangtekst tilgjengelig",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Nu afspelen", "nowPlaying": "Nu afspelen",
"playingFrom": "Afspelen vanaf ${title}", "playingFrom": "Afspelen vanaf ${title}",
"queue": "Wachtrij", "queue": "Wachtrij",
"upNext": "Volgende nummers",
"clearQueue": "Wachtrij wissen", "clearQueue": "Wachtrij wissen",
"lyrics": "Songtekst", "lyrics": "Songtekst",
"noLyrics": "Geen songtekst beschikbaar", "noLyrics": "Geen songtekst beschikbaar",
-1
View File
@@ -977,7 +977,6 @@
"nowPlaying": "Teraz odtwarzane", "nowPlaying": "Teraz odtwarzane",
"playingFrom": "Odtwarzanie z ${title}", "playingFrom": "Odtwarzanie z ${title}",
"queue": "Kolejka", "queue": "Kolejka",
"upNext": "Następne",
"clearQueue": "Wyczyść kolejkę", "clearQueue": "Wyczyść kolejkę",
"lyrics": "Tekst", "lyrics": "Tekst",
"noLyrics": "Brak dostępnego tekstu", "noLyrics": "Brak dostępnego tekstu",
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Reproduzindo agora", "nowPlaying": "Reproduzindo agora",
"playingFrom": "Reproduzindo de ${title}", "playingFrom": "Reproduzindo de ${title}",
"queue": "Fila", "queue": "Fila",
"upNext": "A seguir",
"clearQueue": "Limpar fila", "clearQueue": "Limpar fila",
"lyrics": "Letra", "lyrics": "Letra",
"noLyrics": "Nenhuma letra disponível", "noLyrics": "Nenhuma letra disponível",
-1
View File
@@ -977,7 +977,6 @@
"nowPlaying": "Сейчас играет", "nowPlaying": "Сейчас играет",
"playingFrom": "Воспроизведение из ${title}", "playingFrom": "Воспроизведение из ${title}",
"queue": "Очередь", "queue": "Очередь",
"upNext": "Далее",
"clearQueue": "Очистить очередь", "clearQueue": "Очистить очередь",
"lyrics": "Текст песни", "lyrics": "Текст песни",
"noLyrics": "Текст песни недоступен", "noLyrics": "Текст песни недоступен",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 16 /// Locales: 16
/// Strings: 21376 (1336 per locale) /// Strings: 21360 (1335 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicBg extends TranslationsMusicEn {
@override String get nowPlaying => 'Сега се възпроизвежда'; @override String get nowPlaying => 'Сега се възпроизвежда';
@override String playingFrom({required Object title}) => 'Възпроизвеждане от ${title}'; @override String playingFrom({required Object title}) => 'Възпроизвеждане от ${title}';
@override String get queue => 'Опашка'; @override String get queue => 'Опашка';
@override String get upNext => 'Следва';
@override String get clearQueue => 'Изчисти опашката'; @override String get clearQueue => 'Изчисти опашката';
@override String get lyrics => 'Текст'; @override String get lyrics => 'Текст';
@override String get noLyrics => 'Няма наличен текст'; @override String get noLyrics => 'Няма наличен текст';
@@ -2892,7 +2891,6 @@ extension on TranslationsBg {
'music.nowPlaying' => 'Сега се възпроизвежда', 'music.nowPlaying' => 'Сега се възпроизвежда',
'music.playingFrom' => ({required Object title}) => 'Възпроизвеждане от ${title}', 'music.playingFrom' => ({required Object title}) => 'Възпроизвеждане от ${title}',
'music.queue' => 'Опашка', 'music.queue' => 'Опашка',
'music.upNext' => 'Следва',
'music.clearQueue' => 'Изчисти опашката', 'music.clearQueue' => 'Изчисти опашката',
'music.lyrics' => 'Текст', 'music.lyrics' => 'Текст',
'music.noLyrics' => 'Няма наличен текст', 'music.noLyrics' => 'Няма наличен текст',
@@ -3022,9 +3020,9 @@ extension on TranslationsBg {
'downloads.noSyncRules' => 'Няма правила за синхронизация', 'downloads.noSyncRules' => 'Няма правила за синхронизация',
'downloads.manageSyncRule' => 'Управление на синхронизацията', 'downloads.manageSyncRule' => 'Управление на синхронизацията',
'downloads.editEpisodeCount' => 'Брой епизоди', 'downloads.editEpisodeCount' => 'Брой епизоди',
'downloads.editSyncFilter' => 'Филтър за синхронизация',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Филтър за синхронизация',
'downloads.syncAllItems' => 'Синхронизират се всички елементи', 'downloads.syncAllItems' => 'Синхронизират се всички елементи',
'downloads.syncUnwatchedItems' => 'Синхронизират се негледаните елементи', 'downloads.syncUnwatchedItems' => 'Синхронизират се негледаните елементи',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Сървър: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Сървър: ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicDa extends TranslationsMusicEn {
@override String get nowPlaying => 'Afspiller nu'; @override String get nowPlaying => 'Afspiller nu';
@override String playingFrom({required Object title}) => 'Afspiller fra ${title}'; @override String playingFrom({required Object title}) => 'Afspiller fra ${title}';
@override String get queue => ''; @override String get queue => '';
@override String get upNext => 'Næste i køen';
@override String get clearQueue => 'Ryd kø'; @override String get clearQueue => 'Ryd kø';
@override String get lyrics => 'Sangtekst'; @override String get lyrics => 'Sangtekst';
@override String get noLyrics => 'Ingen sangtekst tilgængelig'; @override String get noLyrics => 'Ingen sangtekst tilgængelig';
@@ -2892,7 +2891,6 @@ extension on TranslationsDa {
'music.nowPlaying' => 'Afspiller nu', 'music.nowPlaying' => 'Afspiller nu',
'music.playingFrom' => ({required Object title}) => 'Afspiller fra ${title}', 'music.playingFrom' => ({required Object title}) => 'Afspiller fra ${title}',
'music.queue' => '', 'music.queue' => '',
'music.upNext' => 'Næste i køen',
'music.clearQueue' => 'Ryd kø', 'music.clearQueue' => 'Ryd kø',
'music.lyrics' => 'Sangtekst', 'music.lyrics' => 'Sangtekst',
'music.noLyrics' => 'Ingen sangtekst tilgængelig', 'music.noLyrics' => 'Ingen sangtekst tilgængelig',
@@ -3022,9 +3020,9 @@ extension on TranslationsDa {
'downloads.noSyncRules' => 'Ingen synkroniseringsregler', 'downloads.noSyncRules' => 'Ingen synkroniseringsregler',
'downloads.manageSyncRule' => 'Administrer synkronisering', 'downloads.manageSyncRule' => 'Administrer synkronisering',
'downloads.editEpisodeCount' => 'Antal episoder', 'downloads.editEpisodeCount' => 'Antal episoder',
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
'downloads.syncAllItems' => 'Synkroniserer alle elementer', 'downloads.syncAllItems' => 'Synkroniserer alle elementer',
'downloads.syncUnwatchedItems' => 'Synkroniserer usete elementer', 'downloads.syncUnwatchedItems' => 'Synkroniserer usete elementer',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicDe extends TranslationsMusicEn {
@override String get nowPlaying => 'Wird wiedergegeben'; @override String get nowPlaying => 'Wird wiedergegeben';
@override String playingFrom({required Object title}) => 'Wiedergabe aus ${title}'; @override String playingFrom({required Object title}) => 'Wiedergabe aus ${title}';
@override String get queue => 'Warteschlange'; @override String get queue => 'Warteschlange';
@override String get upNext => 'Als Nächstes';
@override String get clearQueue => 'Warteschlange leeren'; @override String get clearQueue => 'Warteschlange leeren';
@override String get lyrics => 'Songtext'; @override String get lyrics => 'Songtext';
@override String get noLyrics => 'Kein Songtext verfügbar'; @override String get noLyrics => 'Kein Songtext verfügbar';
@@ -2892,7 +2891,6 @@ extension on TranslationsDe {
'music.nowPlaying' => 'Wird wiedergegeben', 'music.nowPlaying' => 'Wird wiedergegeben',
'music.playingFrom' => ({required Object title}) => 'Wiedergabe aus ${title}', 'music.playingFrom' => ({required Object title}) => 'Wiedergabe aus ${title}',
'music.queue' => 'Warteschlange', 'music.queue' => 'Warteschlange',
'music.upNext' => 'Als Nächstes',
'music.clearQueue' => 'Warteschlange leeren', 'music.clearQueue' => 'Warteschlange leeren',
'music.lyrics' => 'Songtext', 'music.lyrics' => 'Songtext',
'music.noLyrics' => 'Kein Songtext verfügbar', 'music.noLyrics' => 'Kein Songtext verfügbar',
@@ -3022,9 +3020,9 @@ extension on TranslationsDe {
'downloads.noSyncRules' => 'Keine Sync-Regeln', 'downloads.noSyncRules' => 'Keine Sync-Regeln',
'downloads.manageSyncRule' => 'Synchronisierung verwalten', 'downloads.manageSyncRule' => 'Synchronisierung verwalten',
'downloads.editEpisodeCount' => 'Episodenanzahl', 'downloads.editEpisodeCount' => 'Episodenanzahl',
'downloads.editSyncFilter' => 'Synchronisierungsfilter',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Synchronisierungsfilter',
'downloads.syncAllItems' => 'Alle Einträge synchronisieren', 'downloads.syncAllItems' => 'Alle Einträge synchronisieren',
'downloads.syncUnwatchedItems' => 'Ungesehene Einträge synchronisieren', 'downloads.syncUnwatchedItems' => 'Ungesehene Einträge synchronisieren',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+1 -5
View File
@@ -2905,9 +2905,6 @@ class TranslationsMusicEn {
/// en: 'Queue' /// en: 'Queue'
String get queue => 'Queue'; String get queue => 'Queue';
/// en: 'Up next'
String get upNext => 'Up next';
/// en: 'Clear queue' /// en: 'Clear queue'
String get clearQueue => 'Clear queue'; String get clearQueue => 'Clear queue';
@@ -5623,7 +5620,6 @@ extension on Translations {
'music.nowPlaying' => 'Now Playing', 'music.nowPlaying' => 'Now Playing',
'music.playingFrom' => ({required Object title}) => 'Playing from ${title}', 'music.playingFrom' => ({required Object title}) => 'Playing from ${title}',
'music.queue' => 'Queue', 'music.queue' => 'Queue',
'music.upNext' => 'Up next',
'music.clearQueue' => 'Clear queue', 'music.clearQueue' => 'Clear queue',
'music.lyrics' => 'Lyrics', 'music.lyrics' => 'Lyrics',
'music.noLyrics' => 'No lyrics available', 'music.noLyrics' => 'No lyrics available',
@@ -5741,9 +5737,9 @@ extension on Translations {
'downloads.customAmount' => 'Custom amount...', 'downloads.customAmount' => 'Custom amount...',
'downloads.includeSpecials' => 'Include Specials', 'downloads.includeSpecials' => 'Include Specials',
'downloads.howManyEpisodes' => 'How many episodes?', 'downloads.howManyEpisodes' => 'How many episodes?',
'downloads.itemsQueued' => ({required Object count}) => '${count} items queued for download',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.itemsQueued' => ({required Object count}) => '${count} items queued for download',
'downloads.keepSynced' => 'Keep synced', 'downloads.keepSynced' => 'Keep synced',
'downloads.downloadOnce' => 'Download once', 'downloads.downloadOnce' => 'Download once',
'downloads.keepNUnwatched' => ({required Object count}) => 'Keep ${count} unwatched', 'downloads.keepNUnwatched' => ({required Object count}) => 'Keep ${count} unwatched',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicEs extends TranslationsMusicEn {
@override String get nowPlaying => 'Reproduciendo ahora'; @override String get nowPlaying => 'Reproduciendo ahora';
@override String playingFrom({required Object title}) => 'Reproduciendo desde ${title}'; @override String playingFrom({required Object title}) => 'Reproduciendo desde ${title}';
@override String get queue => 'Cola'; @override String get queue => 'Cola';
@override String get upNext => 'A continuación';
@override String get clearQueue => 'Vaciar la cola'; @override String get clearQueue => 'Vaciar la cola';
@override String get lyrics => 'Letra'; @override String get lyrics => 'Letra';
@override String get noLyrics => 'No hay letra disponible'; @override String get noLyrics => 'No hay letra disponible';
@@ -2892,7 +2891,6 @@ extension on TranslationsEs {
'music.nowPlaying' => 'Reproduciendo ahora', 'music.nowPlaying' => 'Reproduciendo ahora',
'music.playingFrom' => ({required Object title}) => 'Reproduciendo desde ${title}', 'music.playingFrom' => ({required Object title}) => 'Reproduciendo desde ${title}',
'music.queue' => 'Cola', 'music.queue' => 'Cola',
'music.upNext' => 'A continuación',
'music.clearQueue' => 'Vaciar la cola', 'music.clearQueue' => 'Vaciar la cola',
'music.lyrics' => 'Letra', 'music.lyrics' => 'Letra',
'music.noLyrics' => 'No hay letra disponible', 'music.noLyrics' => 'No hay letra disponible',
@@ -3022,9 +3020,9 @@ extension on TranslationsEs {
'downloads.noSyncRules' => 'Sin reglas de sincronización', 'downloads.noSyncRules' => 'Sin reglas de sincronización',
'downloads.manageSyncRule' => 'Gestionar sincronización', 'downloads.manageSyncRule' => 'Gestionar sincronización',
'downloads.editEpisodeCount' => 'Número de episodios', 'downloads.editEpisodeCount' => 'Número de episodios',
'downloads.editSyncFilter' => 'Filtro de sincronización',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Filtro de sincronización',
'downloads.syncAllItems' => 'Sincronizando todos los elementos', 'downloads.syncAllItems' => 'Sincronizando todos los elementos',
'downloads.syncUnwatchedItems' => 'Sincronizando elementos no vistos', 'downloads.syncUnwatchedItems' => 'Sincronizando elementos no vistos',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Servidor: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Servidor: ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicFr extends TranslationsMusicEn {
@override String get nowPlaying => 'Lecture en cours'; @override String get nowPlaying => 'Lecture en cours';
@override String playingFrom({required Object title}) => 'Lecture depuis ${title}'; @override String playingFrom({required Object title}) => 'Lecture depuis ${title}';
@override String get queue => 'File d\'attente'; @override String get queue => 'File d\'attente';
@override String get upNext => 'À suivre';
@override String get clearQueue => 'Vider la file d\'attente'; @override String get clearQueue => 'Vider la file d\'attente';
@override String get lyrics => 'Paroles'; @override String get lyrics => 'Paroles';
@override String get noLyrics => 'Aucune parole disponible'; @override String get noLyrics => 'Aucune parole disponible';
@@ -2892,7 +2891,6 @@ extension on TranslationsFr {
'music.nowPlaying' => 'Lecture en cours', 'music.nowPlaying' => 'Lecture en cours',
'music.playingFrom' => ({required Object title}) => 'Lecture depuis ${title}', 'music.playingFrom' => ({required Object title}) => 'Lecture depuis ${title}',
'music.queue' => 'File d\'attente', 'music.queue' => 'File d\'attente',
'music.upNext' => 'À suivre',
'music.clearQueue' => 'Vider la file d\'attente', 'music.clearQueue' => 'Vider la file d\'attente',
'music.lyrics' => 'Paroles', 'music.lyrics' => 'Paroles',
'music.noLyrics' => 'Aucune parole disponible', 'music.noLyrics' => 'Aucune parole disponible',
@@ -3022,9 +3020,9 @@ extension on TranslationsFr {
'downloads.noSyncRules' => 'Aucune règle de synchronisation', 'downloads.noSyncRules' => 'Aucune règle de synchronisation',
'downloads.manageSyncRule' => 'Gérer la synchronisation', 'downloads.manageSyncRule' => 'Gérer la synchronisation',
'downloads.editEpisodeCount' => 'Nombre d’épisodes', 'downloads.editEpisodeCount' => 'Nombre d’épisodes',
'downloads.editSyncFilter' => 'Filtre de synchronisation',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Filtre de synchronisation',
'downloads.syncAllItems' => 'Synchronisation de tous les éléments', 'downloads.syncAllItems' => 'Synchronisation de tous les éléments',
'downloads.syncUnwatchedItems' => 'Synchronisation des éléments non vus', 'downloads.syncUnwatchedItems' => 'Synchronisation des éléments non vus',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Serveur : ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Serveur : ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicIt extends TranslationsMusicEn {
@override String get nowPlaying => 'In riproduzione'; @override String get nowPlaying => 'In riproduzione';
@override String playingFrom({required Object title}) => 'Riproduzione da ${title}'; @override String playingFrom({required Object title}) => 'Riproduzione da ${title}';
@override String get queue => 'Coda'; @override String get queue => 'Coda';
@override String get upNext => 'Da riprodurre';
@override String get clearQueue => 'Svuota la coda'; @override String get clearQueue => 'Svuota la coda';
@override String get lyrics => 'Testo'; @override String get lyrics => 'Testo';
@override String get noLyrics => 'Nessun testo disponibile'; @override String get noLyrics => 'Nessun testo disponibile';
@@ -2892,7 +2891,6 @@ extension on TranslationsIt {
'music.nowPlaying' => 'In riproduzione', 'music.nowPlaying' => 'In riproduzione',
'music.playingFrom' => ({required Object title}) => 'Riproduzione da ${title}', 'music.playingFrom' => ({required Object title}) => 'Riproduzione da ${title}',
'music.queue' => 'Coda', 'music.queue' => 'Coda',
'music.upNext' => 'Da riprodurre',
'music.clearQueue' => 'Svuota la coda', 'music.clearQueue' => 'Svuota la coda',
'music.lyrics' => 'Testo', 'music.lyrics' => 'Testo',
'music.noLyrics' => 'Nessun testo disponibile', 'music.noLyrics' => 'Nessun testo disponibile',
@@ -3022,9 +3020,9 @@ extension on TranslationsIt {
'downloads.noSyncRules' => 'Nessuna regola di sincronizzazione', 'downloads.noSyncRules' => 'Nessuna regola di sincronizzazione',
'downloads.manageSyncRule' => 'Gestisci sincronizzazione', 'downloads.manageSyncRule' => 'Gestisci sincronizzazione',
'downloads.editEpisodeCount' => 'Numero di episodi', 'downloads.editEpisodeCount' => 'Numero di episodi',
'downloads.editSyncFilter' => 'Filtro di sincronizzazione',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Filtro di sincronizzazione',
'downloads.syncAllItems' => 'Sincronizzazione di tutti gli elementi', 'downloads.syncAllItems' => 'Sincronizzazione di tutti gli elementi',
'downloads.syncUnwatchedItems' => 'Sincronizzazione degli elementi non visti', 'downloads.syncUnwatchedItems' => 'Sincronizzazione degli elementi non visti',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+1 -3
View File
@@ -1222,7 +1222,6 @@ class _TranslationsMusicJa extends TranslationsMusicEn {
@override String get nowPlaying => '再生中'; @override String get nowPlaying => '再生中';
@override String playingFrom({required Object title}) => '${title} から再生'; @override String playingFrom({required Object title}) => '${title} から再生';
@override String get queue => '再生キュー'; @override String get queue => '再生キュー';
@override String get upNext => '次はこちら';
@override String get clearQueue => 'キューをクリア'; @override String get clearQueue => 'キューをクリア';
@override String get lyrics => '歌詞'; @override String get lyrics => '歌詞';
@override String get noLyrics => '歌詞がありません'; @override String get noLyrics => '歌詞がありません';
@@ -2891,7 +2890,6 @@ extension on TranslationsJa {
'music.nowPlaying' => '再生中', 'music.nowPlaying' => '再生中',
'music.playingFrom' => ({required Object title}) => '${title} から再生', 'music.playingFrom' => ({required Object title}) => '${title} から再生',
'music.queue' => '再生キュー', 'music.queue' => '再生キュー',
'music.upNext' => '次はこちら',
'music.clearQueue' => 'キューをクリア', 'music.clearQueue' => 'キューをクリア',
'music.lyrics' => '歌詞', 'music.lyrics' => '歌詞',
'music.noLyrics' => '歌詞がありません', 'music.noLyrics' => '歌詞がありません',
@@ -3021,9 +3019,9 @@ extension on TranslationsJa {
'downloads.noSyncRules' => '同期ルールなし', 'downloads.noSyncRules' => '同期ルールなし',
'downloads.manageSyncRule' => '同期を管理', 'downloads.manageSyncRule' => '同期を管理',
'downloads.editEpisodeCount' => 'エピソード数', 'downloads.editEpisodeCount' => 'エピソード数',
'downloads.editSyncFilter' => '同期フィルター',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => '同期フィルター',
'downloads.syncAllItems' => 'すべてのアイテムを同期中', 'downloads.syncAllItems' => 'すべてのアイテムを同期中',
'downloads.syncUnwatchedItems' => '未視聴のアイテムを同期中', 'downloads.syncUnwatchedItems' => '未視聴のアイテムを同期中',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'サーバー: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'サーバー: ${server}${status}',
+1 -3
View File
@@ -1222,7 +1222,6 @@ class _TranslationsMusicKo extends TranslationsMusicEn {
@override String get nowPlaying => '재생 중'; @override String get nowPlaying => '재생 중';
@override String playingFrom({required Object title}) => '${title}에서 재생 중'; @override String playingFrom({required Object title}) => '${title}에서 재생 중';
@override String get queue => '재생 대기열'; @override String get queue => '재생 대기열';
@override String get upNext => '다음 항목';
@override String get clearQueue => '대기열 비우기'; @override String get clearQueue => '대기열 비우기';
@override String get lyrics => '가사'; @override String get lyrics => '가사';
@override String get noLyrics => '가사 없음'; @override String get noLyrics => '가사 없음';
@@ -2891,7 +2890,6 @@ extension on TranslationsKo {
'music.nowPlaying' => '재생 중', 'music.nowPlaying' => '재생 중',
'music.playingFrom' => ({required Object title}) => '${title}에서 재생 중', 'music.playingFrom' => ({required Object title}) => '${title}에서 재생 중',
'music.queue' => '재생 대기열', 'music.queue' => '재생 대기열',
'music.upNext' => '다음 항목',
'music.clearQueue' => '대기열 비우기', 'music.clearQueue' => '대기열 비우기',
'music.lyrics' => '가사', 'music.lyrics' => '가사',
'music.noLyrics' => '가사 없음', 'music.noLyrics' => '가사 없음',
@@ -3021,9 +3019,9 @@ extension on TranslationsKo {
'downloads.noSyncRules' => '동기화 규칙 없음', 'downloads.noSyncRules' => '동기화 규칙 없음',
'downloads.manageSyncRule' => '동기화 관리', 'downloads.manageSyncRule' => '동기화 관리',
'downloads.editEpisodeCount' => '에피소드 수', 'downloads.editEpisodeCount' => '에피소드 수',
'downloads.editSyncFilter' => '동기화 필터',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => '동기화 필터',
'downloads.syncAllItems' => '모든 항목 동기화 중', 'downloads.syncAllItems' => '모든 항목 동기화 중',
'downloads.syncUnwatchedItems' => '시청하지 않은 항목 동기화 중', 'downloads.syncUnwatchedItems' => '시청하지 않은 항목 동기화 중',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => '서버: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => '서버: ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicNb extends TranslationsMusicEn {
@override String get nowPlaying => 'Spilles nå'; @override String get nowPlaying => 'Spilles nå';
@override String playingFrom({required Object title}) => 'Spiller fra ${title}'; @override String playingFrom({required Object title}) => 'Spiller fra ${title}';
@override String get queue => ''; @override String get queue => '';
@override String get upNext => 'Neste i køen';
@override String get clearQueue => 'Tøm kø'; @override String get clearQueue => 'Tøm kø';
@override String get lyrics => 'Sangtekst'; @override String get lyrics => 'Sangtekst';
@override String get noLyrics => 'Ingen sangtekst tilgjengelig'; @override String get noLyrics => 'Ingen sangtekst tilgjengelig';
@@ -2892,7 +2891,6 @@ extension on TranslationsNb {
'music.nowPlaying' => 'Spilles nå', 'music.nowPlaying' => 'Spilles nå',
'music.playingFrom' => ({required Object title}) => 'Spiller fra ${title}', 'music.playingFrom' => ({required Object title}) => 'Spiller fra ${title}',
'music.queue' => '', 'music.queue' => '',
'music.upNext' => 'Neste i køen',
'music.clearQueue' => 'Tøm kø', 'music.clearQueue' => 'Tøm kø',
'music.lyrics' => 'Sangtekst', 'music.lyrics' => 'Sangtekst',
'music.noLyrics' => 'Ingen sangtekst tilgjengelig', 'music.noLyrics' => 'Ingen sangtekst tilgjengelig',
@@ -3022,9 +3020,9 @@ extension on TranslationsNb {
'downloads.noSyncRules' => 'Ingen synkroniseringsregler', 'downloads.noSyncRules' => 'Ingen synkroniseringsregler',
'downloads.manageSyncRule' => 'Administrer synkronisering', 'downloads.manageSyncRule' => 'Administrer synkronisering',
'downloads.editEpisodeCount' => 'Antall episoder', 'downloads.editEpisodeCount' => 'Antall episoder',
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
'downloads.syncAllItems' => 'Synkroniserer alle elementer', 'downloads.syncAllItems' => 'Synkroniserer alle elementer',
'downloads.syncUnwatchedItems' => 'Synkroniserer usette elementer', 'downloads.syncUnwatchedItems' => 'Synkroniserer usette elementer',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicNl extends TranslationsMusicEn {
@override String get nowPlaying => 'Nu afspelen'; @override String get nowPlaying => 'Nu afspelen';
@override String playingFrom({required Object title}) => 'Afspelen vanaf ${title}'; @override String playingFrom({required Object title}) => 'Afspelen vanaf ${title}';
@override String get queue => 'Wachtrij'; @override String get queue => 'Wachtrij';
@override String get upNext => 'Volgende nummers';
@override String get clearQueue => 'Wachtrij wissen'; @override String get clearQueue => 'Wachtrij wissen';
@override String get lyrics => 'Songtekst'; @override String get lyrics => 'Songtekst';
@override String get noLyrics => 'Geen songtekst beschikbaar'; @override String get noLyrics => 'Geen songtekst beschikbaar';
@@ -2892,7 +2891,6 @@ extension on TranslationsNl {
'music.nowPlaying' => 'Nu afspelen', 'music.nowPlaying' => 'Nu afspelen',
'music.playingFrom' => ({required Object title}) => 'Afspelen vanaf ${title}', 'music.playingFrom' => ({required Object title}) => 'Afspelen vanaf ${title}',
'music.queue' => 'Wachtrij', 'music.queue' => 'Wachtrij',
'music.upNext' => 'Volgende nummers',
'music.clearQueue' => 'Wachtrij wissen', 'music.clearQueue' => 'Wachtrij wissen',
'music.lyrics' => 'Songtekst', 'music.lyrics' => 'Songtekst',
'music.noLyrics' => 'Geen songtekst beschikbaar', 'music.noLyrics' => 'Geen songtekst beschikbaar',
@@ -3022,9 +3020,9 @@ extension on TranslationsNl {
'downloads.noSyncRules' => 'Geen synchronisatieregels', 'downloads.noSyncRules' => 'Geen synchronisatieregels',
'downloads.manageSyncRule' => 'Synchronisatie beheren', 'downloads.manageSyncRule' => 'Synchronisatie beheren',
'downloads.editEpisodeCount' => 'Aantal afleveringen', 'downloads.editEpisodeCount' => 'Aantal afleveringen',
'downloads.editSyncFilter' => 'Synchronisatiefilter',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Synchronisatiefilter',
'downloads.syncAllItems' => 'Alle items synchroniseren', 'downloads.syncAllItems' => 'Alle items synchroniseren',
'downloads.syncUnwatchedItems' => 'Ongekeken items synchroniseren', 'downloads.syncUnwatchedItems' => 'Ongekeken items synchroniseren',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+1 -3
View File
@@ -1225,7 +1225,6 @@ class _TranslationsMusicPl extends TranslationsMusicEn {
@override String get nowPlaying => 'Teraz odtwarzane'; @override String get nowPlaying => 'Teraz odtwarzane';
@override String playingFrom({required Object title}) => 'Odtwarzanie z ${title}'; @override String playingFrom({required Object title}) => 'Odtwarzanie z ${title}';
@override String get queue => 'Kolejka'; @override String get queue => 'Kolejka';
@override String get upNext => 'Następne';
@override String get clearQueue => 'Wyczyść kolejkę'; @override String get clearQueue => 'Wyczyść kolejkę';
@override String get lyrics => 'Tekst'; @override String get lyrics => 'Tekst';
@override String get noLyrics => 'Brak dostępnego tekstu'; @override String get noLyrics => 'Brak dostępnego tekstu';
@@ -2894,7 +2893,6 @@ extension on TranslationsPl {
'music.nowPlaying' => 'Teraz odtwarzane', 'music.nowPlaying' => 'Teraz odtwarzane',
'music.playingFrom' => ({required Object title}) => 'Odtwarzanie z ${title}', 'music.playingFrom' => ({required Object title}) => 'Odtwarzanie z ${title}',
'music.queue' => 'Kolejka', 'music.queue' => 'Kolejka',
'music.upNext' => 'Następne',
'music.clearQueue' => 'Wyczyść kolejkę', 'music.clearQueue' => 'Wyczyść kolejkę',
'music.lyrics' => 'Tekst', 'music.lyrics' => 'Tekst',
'music.noLyrics' => 'Brak dostępnego tekstu', 'music.noLyrics' => 'Brak dostępnego tekstu',
@@ -3024,9 +3022,9 @@ extension on TranslationsPl {
'downloads.noSyncRules' => 'Brak reguł synchronizacji', 'downloads.noSyncRules' => 'Brak reguł synchronizacji',
'downloads.manageSyncRule' => 'Zarządzaj synchronizacją', 'downloads.manageSyncRule' => 'Zarządzaj synchronizacją',
'downloads.editEpisodeCount' => 'Liczba odcinków', 'downloads.editEpisodeCount' => 'Liczba odcinków',
'downloads.editSyncFilter' => 'Filtr synchronizacji',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Filtr synchronizacji',
'downloads.syncAllItems' => 'Synchronizuję wszystkie elementy', 'downloads.syncAllItems' => 'Synchronizuję wszystkie elementy',
'downloads.syncUnwatchedItems' => 'Synchronizuję nieobejrzane elementy', 'downloads.syncUnwatchedItems' => 'Synchronizuję nieobejrzane elementy',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Serwer: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Serwer: ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicPt extends TranslationsMusicEn {
@override String get nowPlaying => 'Reproduzindo agora'; @override String get nowPlaying => 'Reproduzindo agora';
@override String playingFrom({required Object title}) => 'Reproduzindo de ${title}'; @override String playingFrom({required Object title}) => 'Reproduzindo de ${title}';
@override String get queue => 'Fila'; @override String get queue => 'Fila';
@override String get upNext => 'A seguir';
@override String get clearQueue => 'Limpar fila'; @override String get clearQueue => 'Limpar fila';
@override String get lyrics => 'Letra'; @override String get lyrics => 'Letra';
@override String get noLyrics => 'Nenhuma letra disponível'; @override String get noLyrics => 'Nenhuma letra disponível';
@@ -2892,7 +2891,6 @@ extension on TranslationsPt {
'music.nowPlaying' => 'Reproduzindo agora', 'music.nowPlaying' => 'Reproduzindo agora',
'music.playingFrom' => ({required Object title}) => 'Reproduzindo de ${title}', 'music.playingFrom' => ({required Object title}) => 'Reproduzindo de ${title}',
'music.queue' => 'Fila', 'music.queue' => 'Fila',
'music.upNext' => 'A seguir',
'music.clearQueue' => 'Limpar fila', 'music.clearQueue' => 'Limpar fila',
'music.lyrics' => 'Letra', 'music.lyrics' => 'Letra',
'music.noLyrics' => 'Nenhuma letra disponível', 'music.noLyrics' => 'Nenhuma letra disponível',
@@ -3022,9 +3020,9 @@ extension on TranslationsPt {
'downloads.noSyncRules' => 'Nenhuma regra de sincronização', 'downloads.noSyncRules' => 'Nenhuma regra de sincronização',
'downloads.manageSyncRule' => 'Gerenciar sincronização', 'downloads.manageSyncRule' => 'Gerenciar sincronização',
'downloads.editEpisodeCount' => 'Número de episódios', 'downloads.editEpisodeCount' => 'Número de episódios',
'downloads.editSyncFilter' => 'Filtro de sincronização',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Filtro de sincronização',
'downloads.syncAllItems' => 'Sincronizando todos os itens', 'downloads.syncAllItems' => 'Sincronizando todos os itens',
'downloads.syncUnwatchedItems' => 'Sincronizando itens não vistos', 'downloads.syncUnwatchedItems' => 'Sincronizando itens não vistos',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Servidor: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Servidor: ${server}${status}',
+1 -3
View File
@@ -1225,7 +1225,6 @@ class _TranslationsMusicRu extends TranslationsMusicEn {
@override String get nowPlaying => 'Сейчас играет'; @override String get nowPlaying => 'Сейчас играет';
@override String playingFrom({required Object title}) => 'Воспроизведение из ${title}'; @override String playingFrom({required Object title}) => 'Воспроизведение из ${title}';
@override String get queue => 'Очередь'; @override String get queue => 'Очередь';
@override String get upNext => 'Далее';
@override String get clearQueue => 'Очистить очередь'; @override String get clearQueue => 'Очистить очередь';
@override String get lyrics => 'Текст песни'; @override String get lyrics => 'Текст песни';
@override String get noLyrics => 'Текст песни недоступен'; @override String get noLyrics => 'Текст песни недоступен';
@@ -2894,7 +2893,6 @@ extension on TranslationsRu {
'music.nowPlaying' => 'Сейчас играет', 'music.nowPlaying' => 'Сейчас играет',
'music.playingFrom' => ({required Object title}) => 'Воспроизведение из ${title}', 'music.playingFrom' => ({required Object title}) => 'Воспроизведение из ${title}',
'music.queue' => 'Очередь', 'music.queue' => 'Очередь',
'music.upNext' => 'Далее',
'music.clearQueue' => 'Очистить очередь', 'music.clearQueue' => 'Очистить очередь',
'music.lyrics' => 'Текст песни', 'music.lyrics' => 'Текст песни',
'music.noLyrics' => 'Текст песни недоступен', 'music.noLyrics' => 'Текст песни недоступен',
@@ -3024,9 +3022,9 @@ extension on TranslationsRu {
'downloads.noSyncRules' => 'Нет правил синхронизации', 'downloads.noSyncRules' => 'Нет правил синхронизации',
'downloads.manageSyncRule' => 'Управление синхронизацией', 'downloads.manageSyncRule' => 'Управление синхронизацией',
'downloads.editEpisodeCount' => 'Количество эпизодов', 'downloads.editEpisodeCount' => 'Количество эпизодов',
'downloads.editSyncFilter' => 'Фильтр синхронизации',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Фильтр синхронизации',
'downloads.syncAllItems' => 'Синхронизация всех элементов', 'downloads.syncAllItems' => 'Синхронизация всех элементов',
'downloads.syncUnwatchedItems' => 'Синхронизация непросмотренных элементов', 'downloads.syncUnwatchedItems' => 'Синхронизация непросмотренных элементов',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Сервер: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Сервер: ${server}${status}',
+1 -3
View File
@@ -1223,7 +1223,6 @@ class _TranslationsMusicSv extends TranslationsMusicEn {
@override String get nowPlaying => 'Spelas nu'; @override String get nowPlaying => 'Spelas nu';
@override String playingFrom({required Object title}) => 'Spelar från ${title}'; @override String playingFrom({required Object title}) => 'Spelar från ${title}';
@override String get queue => ''; @override String get queue => '';
@override String get upNext => 'Näst på tur';
@override String get clearQueue => 'Rensa kön'; @override String get clearQueue => 'Rensa kön';
@override String get lyrics => 'Låttext'; @override String get lyrics => 'Låttext';
@override String get noLyrics => 'Ingen låttext tillgänglig'; @override String get noLyrics => 'Ingen låttext tillgänglig';
@@ -2892,7 +2891,6 @@ extension on TranslationsSv {
'music.nowPlaying' => 'Spelas nu', 'music.nowPlaying' => 'Spelas nu',
'music.playingFrom' => ({required Object title}) => 'Spelar från ${title}', 'music.playingFrom' => ({required Object title}) => 'Spelar från ${title}',
'music.queue' => '', 'music.queue' => '',
'music.upNext' => 'Näst på tur',
'music.clearQueue' => 'Rensa kön', 'music.clearQueue' => 'Rensa kön',
'music.lyrics' => 'Låttext', 'music.lyrics' => 'Låttext',
'music.noLyrics' => 'Ingen låttext tillgänglig', 'music.noLyrics' => 'Ingen låttext tillgänglig',
@@ -3022,9 +3020,9 @@ extension on TranslationsSv {
'downloads.noSyncRules' => 'Inga synkregler', 'downloads.noSyncRules' => 'Inga synkregler',
'downloads.manageSyncRule' => 'Hantera synkronisering', 'downloads.manageSyncRule' => 'Hantera synkronisering',
'downloads.editEpisodeCount' => 'Antal avsnitt', 'downloads.editEpisodeCount' => 'Antal avsnitt',
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => 'Synkroniseringsfilter',
'downloads.syncAllItems' => 'Synkroniserar alla objekt', 'downloads.syncAllItems' => 'Synkroniserar alla objekt',
'downloads.syncUnwatchedItems' => 'Synkroniserar osedda objekt', 'downloads.syncUnwatchedItems' => 'Synkroniserar osedda objekt',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => 'Server: ${server}${status}',
+1 -3
View File
@@ -1222,7 +1222,6 @@ class _TranslationsMusicZh extends TranslationsMusicEn {
@override String get nowPlaying => '正在播放'; @override String get nowPlaying => '正在播放';
@override String playingFrom({required Object title}) => '来自 ${title}'; @override String playingFrom({required Object title}) => '来自 ${title}';
@override String get queue => '播放队列'; @override String get queue => '播放队列';
@override String get upNext => '即将播放';
@override String get clearQueue => '清空队列'; @override String get clearQueue => '清空队列';
@override String get lyrics => '歌词'; @override String get lyrics => '歌词';
@override String get noLyrics => '暂无歌词'; @override String get noLyrics => '暂无歌词';
@@ -2891,7 +2890,6 @@ extension on TranslationsZh {
'music.nowPlaying' => '正在播放', 'music.nowPlaying' => '正在播放',
'music.playingFrom' => ({required Object title}) => '来自 ${title}', 'music.playingFrom' => ({required Object title}) => '来自 ${title}',
'music.queue' => '播放队列', 'music.queue' => '播放队列',
'music.upNext' => '即将播放',
'music.clearQueue' => '清空队列', 'music.clearQueue' => '清空队列',
'music.lyrics' => '歌词', 'music.lyrics' => '歌词',
'music.noLyrics' => '暂无歌词', 'music.noLyrics' => '暂无歌词',
@@ -3021,9 +3019,9 @@ extension on TranslationsZh {
'downloads.noSyncRules' => '没有同步规则', 'downloads.noSyncRules' => '没有同步规则',
'downloads.manageSyncRule' => '管理同步', 'downloads.manageSyncRule' => '管理同步',
'downloads.editEpisodeCount' => '剧集数量', 'downloads.editEpisodeCount' => '剧集数量',
'downloads.editSyncFilter' => '同步筛选',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.editSyncFilter' => '同步筛选',
'downloads.syncAllItems' => '同步所有项目', 'downloads.syncAllItems' => '同步所有项目',
'downloads.syncUnwatchedItems' => '同步未观看项目', 'downloads.syncUnwatchedItems' => '同步未观看项目',
'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => '服务器: ${server}${status}', 'downloads.syncRuleServerContext' => ({required Object server, required Object status}) => '服务器: ${server}${status}',
-1
View File
@@ -975,7 +975,6 @@
"nowPlaying": "Spelas nu", "nowPlaying": "Spelas nu",
"playingFrom": "Spelar från ${title}", "playingFrom": "Spelar från ${title}",
"queue": "Kö", "queue": "Kö",
"upNext": "Näst på tur",
"clearQueue": "Rensa kön", "clearQueue": "Rensa kön",
"lyrics": "Låttext", "lyrics": "Låttext",
"noLyrics": "Ingen låttext tillgänglig", "noLyrics": "Ingen låttext tillgänglig",
-1
View File
@@ -974,7 +974,6 @@
"nowPlaying": "正在播放", "nowPlaying": "正在播放",
"playingFrom": "来自 ${title}", "playingFrom": "来自 ${title}",
"queue": "播放队列", "queue": "播放队列",
"upNext": "即将播放",
"clearQueue": "清空队列", "clearQueue": "清空队列",
"lyrics": "歌词", "lyrics": "歌词",
"noLyrics": "暂无歌词", "noLyrics": "暂无歌词",
+139 -156
View File
@@ -4,20 +4,15 @@ import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../focus/input_mode_tracker.dart';
import '../../i18n/strings.g.dart'; import '../../i18n/strings.g.dart';
import '../../media/ids.dart';
import '../../media/media_item.dart';
import '../../services/music/music_playback_service.dart'; import '../../services/music/music_playback_service.dart';
import '../../theme/mono_tokens.dart'; import '../../theme/mono_tokens.dart';
import '../../utils/media_image_helper.dart';
import '../../utils/platform_detector.dart'; import '../../utils/platform_detector.dart';
import '../../utils/provider_extensions.dart';
import '../../widgets/app_icon.dart'; import '../../widgets/app_icon.dart';
import '../../widgets/bottom_sheet_header.dart'; import '../../widgets/bottom_sheet_header.dart';
import '../../widgets/music/equalizer_icon.dart';
import '../../widgets/music/repeat_mode.dart'; import '../../widgets/music/repeat_mode.dart';
import '../../widgets/music/track_row.dart'; import '../../widgets/music/track_row.dart';
import '../../widgets/optimized_media_image.dart';
import '../../widgets/overlay_sheet.dart'; import '../../widgets/overlay_sheet.dart';
/// Open the play-queue sheet. The caller's screen must have an /// Open the play-queue sheet. The caller's screen must have an
@@ -81,23 +76,95 @@ class QueueSheet extends StatelessWidget {
], ],
), ),
), ),
const Flexible(child: QueueList()), const Flexible(child: QueueList(autofocusCurrent: true)),
], ],
); );
} }
} }
/// The queue body: pinned current-track row, "Up next" label, and the /// The queue body: the full play queue in playback order — history, the
/// upcoming tracks as [TrackRow]s. Reused verbatim by the desktop /// current track (equalizer row, dimmed rows above are already played),
/// now-playing layout as an inline panel. /// then upcoming — YouTube-Music style. Opens scrolled so the current track
/// sits at the top, and follows track changes while the user is near the
/// playback position. Reused verbatim by the desktop now-playing layout as
/// an inline panel.
/// ///
/// Touch: long-press drag to reorder (delayed drag listener — TrackRow's /// Touch: long-press drag to reorder (delayed drag listener — TrackRow's
/// context menu is disabled here so the gesture arena stays clean), swipe to /// context menu is disabled here so the gesture arena stays clean), swipe to
/// remove, tap to jump. D-pad: rows are focusable, SELECT jumps, RIGHT /// remove, tap to jump. D-pad: rows are focusable, SELECT jumps, RIGHT
/// reaches a dedicated remove button; reordering is intentionally not /// reaches a dedicated remove button; reordering is intentionally not
/// offered on TV. /// offered on TV. With [autofocusCurrent] (the sheet) initial focus lands on
class QueueList extends StatelessWidget { /// the current track's row instead of the first history row.
const QueueList({super.key}); class QueueList extends StatefulWidget {
/// Land initial d-pad/keyboard focus on the current track's row,
/// overriding the overlay host's focus-first-descendant default. The
/// inline desktop panel keeps this off so building the now-playing screen
/// never steals focus.
final bool autofocusCurrent;
const QueueList({super.key, this.autofocusCurrent = false});
@override
State<QueueList> createState() => _QueueListState();
}
class _QueueListState extends State<QueueList> {
ScrollController? _scrollController;
final _currentRowFocusNode = FocusNode(debugLabel: 'QueueCurrentRowFocus');
int _lastCurrentIndex = -1;
/// Content offset that puts the row at [index] at the top of the viewport
/// (its top gap scrolled away, the list's own padding still visible).
/// Rows are fixed-extent, so this is exact — the shared
/// measure-the-first-item scroll helper can't be used because only the
/// first row lacks a leading gap.
double _rowOffset(int index, double gap) => index * (TrackRow.height + gap);
@override
void initState() {
super.initState();
if (widget.autofocusCurrent) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Timer.run(() {
if (!mounted || !InputModeTracker.isKeyboardMode(context)) return;
// Schedule after the overlay host's _autoFocus second callback so
// we override its focus-first-descendant default.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (_currentRowFocusNode.context != null) _currentRowFocusNode.requestFocus();
});
});
});
}
}
@override
void dispose() {
_scrollController?.dispose();
_currentRowFocusNode.dispose();
super.dispose();
}
/// Keep the current track at the top while playback advances, but only
/// when the list is still parked where the previous track was anchored —
/// once the user scrolls away to browse, moving rows under their pointer
/// invites mis-taps on remove/jump.
void _followCurrentTrack(int currentIndex, double gap) {
if (currentIndex == _lastCurrentIndex) return;
final fromIndex = _lastCurrentIndex;
_lastCurrentIndex = currentIndex;
// First build: the initial scroll offset already targets the row.
if (fromIndex < 0 || currentIndex < 0) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
final controller = _scrollController;
if (!mounted || controller == null || !controller.hasClients) return;
final position = controller.position;
final anchor = _rowOffset(fromIndex, gap).clamp(0.0, position.maxScrollExtent);
if ((anchor - position.pixels).abs() > (TrackRow.height + gap) * 1.5) return;
final target = _rowOffset(currentIndex, gap).clamp(0.0, position.maxScrollExtent);
unawaited(controller.animateTo(target, duration: const Duration(milliseconds: 250), curve: Curves.easeOut));
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -105,161 +172,77 @@ class QueueList extends StatelessWidget {
final service = context.watch<MusicPlaybackService>(); final service = context.watch<MusicPlaybackService>();
final queue = service.queue; final queue = service.queue;
final currentIndex = service.currentIndex; final currentIndex = service.currentIndex;
final current = service.currentTrack;
final upcomingStart = currentIndex + 1; if (queue.isEmpty) {
final upcoming = currentIndex >= 0 && upcomingStart <= queue.length return Padding(
? queue.sublist(upcomingStart) padding: const EdgeInsets.fromLTRB(20, 4, 20, 20),
: const <MediaItem>[]; child: Text(t.messages.noItemsAvailable, style: TextStyle(fontSize: 13, color: tk.textMuted)),
);
}
_scrollController ??= ScrollController(initialScrollOffset: _rowOffset(currentIndex, tk.groupGap));
_followCurrentTrack(currentIndex, tk.groupGap);
// Stable per-item keys that survive reorders; the same track can appear // Stable per-item keys that survive reorders; the same track can appear
// more than once, so disambiguate repeats by occurrence. // more than once, so disambiguate repeats by occurrence.
final seen = <String, int>{}; final seen = <String, int>{};
final itemKeys = [ final itemKeys = [
for (final item in upcoming) '${item.globalKey}#${seen.update(item.globalKey, (v) => v + 1, ifAbsent: () => 0)}', for (final item in queue) '${item.globalKey}#${seen.update(item.globalKey, (v) => v + 1, ifAbsent: () => 0)}',
]; ];
final allowTouchEditing = !PlatformDetector.isTV(); final allowTouchEditing = !PlatformDetector.isTV();
return Column( return ReorderableListView.builder(
mainAxisSize: .min, scrollController: _scrollController,
crossAxisAlignment: .stretch, shrinkWrap: true,
children: [ buildDefaultDragHandles: false,
if (current != null) padding: const EdgeInsets.all(12),
Padding( itemCount: queue.length,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0), // onReorderItem already adjusts newIndex for the removal.
child: _CurrentTrackRow(track: current), onReorderItem: (oldIndex, newIndex) {
), if (oldIndex == newIndex) return;
Padding( service.reorder(oldIndex, newIndex);
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), },
child: Text( itemBuilder: (context, index) {
t.music.upNext, final item = queue[index];
style: Theme.of(context).textTheme.labelLarge?.copyWith(color: tk.textMuted, fontWeight: .w600), final isCurrent = index == currentIndex;
),
),
Flexible(
child: upcoming.isEmpty
? Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 20),
child: Text(t.messages.noItemsAvailable, style: TextStyle(fontSize: 13, color: tk.textMuted)),
)
: ReorderableListView.builder(
shrinkWrap: true,
buildDefaultDragHandles: false,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
itemCount: upcoming.length,
// onReorderItem already adjusts newIndex for the removal.
onReorderItem: (oldIndex, newIndex) {
if (oldIndex == newIndex) return;
service.reorder(upcomingStart + oldIndex, upcomingStart + newIndex);
},
itemBuilder: (context, index) {
final item = upcoming[index];
final queueIndex = upcomingStart + index;
Widget row = TrackRow( Widget row = TrackRow(
item: item, item: item,
showArtist: true, showArtist: true,
isFirst: index == 0, isFirst: index == 0,
isLast: index == upcoming.length - 1, isLast: index == queue.length - 1,
enableContextMenu: false, enableContextMenu: false,
trailingIcon: Symbols.close_rounded, isCurrent: isCurrent,
onTrailingTap: () => service.removeAt(queueIndex), dimmed: index < currentIndex,
onTap: () => unawaited(service.jumpTo(queueIndex)), focusNode: isCurrent ? _currentRowFocusNode : null,
); trailingIcon: Symbols.close_rounded,
onTrailingTap: () => service.removeAt(index),
onTap: () => unawaited(service.jumpTo(index)),
);
if (allowTouchEditing) { if (allowTouchEditing) {
row = ReorderableDelayedDragStartListener(index: index, child: row); row = ReorderableDelayedDragStartListener(index: index, child: row);
row = Dismissible( row = Dismissible(
key: ValueKey('dismiss:${itemKeys[index]}'), key: ValueKey('dismiss:${itemKeys[index]}'),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
background: Container( background: Container(
alignment: .centerRight, alignment: .centerRight,
padding: const EdgeInsets.only(right: 20), padding: const EdgeInsets.only(right: 20),
color: Colors.red, color: Colors.red,
child: const AppIcon(Symbols.delete_rounded, fill: 1, color: Colors.white, size: 20), child: const AppIcon(Symbols.delete_rounded, fill: 1, color: Colors.white, size: 20),
), ),
onDismissed: (_) => service.removeAt(queueIndex), onDismissed: (_) => service.removeAt(index),
child: row, child: row,
); );
} }
return Padding( return Padding(
key: ValueKey(itemKeys[index]), key: ValueKey(itemKeys[index]),
padding: EdgeInsets.only(top: index == 0 ? 0 : tk.groupGap), padding: EdgeInsets.only(top: index == 0 ? 0 : tk.groupGap),
child: row, child: row,
); );
}, },
),
),
],
);
}
}
/// Pinned "now playing" row at the top of the queue: square art, title +
/// artist, and the shared equalizer indicator.
class _CurrentTrackRow extends StatelessWidget {
final MediaItem track;
const _CurrentTrackRow({required this.track});
@override
Widget build(BuildContext context) {
final tk = tokens(context);
final colorScheme = Theme.of(context).colorScheme;
final isPlaying = context.select<MusicPlaybackService, bool>((s) => s.isPlaying);
final client = context.tryGetMediaClientWithFallback(serverIdOrNull(track.serverId));
final artist = track.trackArtistTitle;
return Material(
color: tk.surface,
borderRadius: BorderRadius.circular(tk.radiusLg),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(tk.radiusSm),
child: OptimizedMediaImage(
client: client,
imagePath: track.thumbPath,
imageType: ImageType.square,
width: 44,
height: 44,
fallbackIcon: Symbols.music_note_rounded,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisAlignment: .center,
crossAxisAlignment: .start,
children: [
Text(
track.title ?? '',
maxLines: 1,
overflow: .ellipsis,
style: TextStyle(fontSize: 14, fontWeight: .w600, color: tk.text),
),
if (artist != null && artist.isNotEmpty)
Text(
artist,
maxLines: 1,
overflow: .ellipsis,
style: TextStyle(fontSize: 12, color: tk.textMuted),
),
],
),
),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(right: 8),
child: EqualizerIcon(animate: isPlaying, color: colorScheme.primary),
),
],
),
),
); );
} }
} }
+14 -2
View File
@@ -68,6 +68,15 @@ class TrackRow extends StatefulWidget {
/// opt out so the recognizers don't fight in the gesture arena. /// opt out so the recognizers don't fight in the gesture arena.
final bool enableContextMenu; final bool enableContextMenu;
/// Override the service-derived "is this the current track" check. Hosts
/// that can render the same track at several queue positions (the queue
/// sheet) pass the exact positional match so only the playing entry gets
/// the equalizer; null keeps the by-id fallback for album/track lists.
final bool? isCurrent;
/// Mute the title — the queue sheet marks already-played entries this way.
final bool dimmed;
/// Show a compact download-status indicator (muted [DownloadStatusIcon]) /// Show a compact download-status indicator (muted [DownloadStatusIcon])
/// between the duration and the trailing ⋮ when the track has a download /// between the duration and the trailing ⋮ when the track has a download
/// record. Off by default — surfaces without download affordances (queue /// record. Off by default — surfaces without download affordances (queue
@@ -90,6 +99,8 @@ class TrackRow extends StatefulWidget {
this.trailingIcon, this.trailingIcon,
this.onTrailingTap, this.onTrailingTap,
this.enableContextMenu = true, this.enableContextMenu = true,
this.isCurrent,
this.dimmed = false,
this.showDownloadStatus = false, this.showDownloadStatus = false,
}); });
@@ -177,8 +188,9 @@ class _TrackRowState extends State<TrackRow> with ContextMenuTapMixin<TrackRow>,
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
// Both selects run unconditionally every build (provider contract). // Both selects run unconditionally every build (provider contract).
final isCurrent = context.select<MusicPlaybackService, bool>((s) => s.currentTrack?.id == widget.item.id); final isCurrentById = context.select<MusicPlaybackService, bool>((s) => s.currentTrack?.id == widget.item.id);
final serviceIsPlaying = context.select<MusicPlaybackService, bool>((s) => s.isPlaying); final serviceIsPlaying = context.select<MusicPlaybackService, bool>((s) => s.isPlaying);
final isCurrent = widget.isCurrent ?? isCurrentById;
final showFocus = _hasFocus && InputModeTracker.isKeyboardMode(context); final showFocus = _hasFocus && InputModeTracker.isKeyboardMode(context);
final radii = BorderRadius.vertical( final radii = BorderRadius.vertical(
@@ -239,7 +251,7 @@ class _TrackRowState extends State<TrackRow> with ContextMenuTapMixin<TrackRow>,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: isCurrent ? FontWeight.w600 : FontWeight.w400, fontWeight: isCurrent ? FontWeight.w600 : FontWeight.w400,
color: isCurrent ? tk.text : null, color: isCurrent ? tk.text : (widget.dimmed ? tk.textMuted : null),
), ),
), ),
if (subtitle != null) if (subtitle != null)
+11 -11
View File
@@ -30,7 +30,8 @@ MediaItem _track(String id, String title) => MediaItem(
serverName: 'Server', serverName: 'Server',
); );
/// Fixed-state fake queue: three tracks, playing the first. /// Fixed-state fake queue: three tracks, playing the middle one — so the
/// sheet renders history, current, and upcoming rows at once.
class _FakeQueueService extends StubMusicPlaybackService { class _FakeQueueService extends StubMusicPlaybackService {
final List<MediaItem> tracks; final List<MediaItem> tracks;
final List<int> jumps = []; final List<int> jumps = [];
@@ -41,7 +42,7 @@ class _FakeQueueService extends StubMusicPlaybackService {
bool get isAvailable => true; bool get isAvailable => true;
@override @override
MediaItem? get currentTrack => tracks.first; MediaItem? get currentTrack => tracks[1];
@override @override
MusicPlaybackStatus get status => MusicPlaybackStatus.playing; MusicPlaybackStatus get status => MusicPlaybackStatus.playing;
@@ -50,7 +51,7 @@ class _FakeQueueService extends StubMusicPlaybackService {
List<MediaItem> get queue => tracks; List<MediaItem> get queue => tracks;
@override @override
int get currentIndex => 0; int get currentIndex => 1;
@override @override
Future<void> jumpTo(int index) async { Future<void> jumpTo(int index) async {
@@ -87,7 +88,7 @@ void main() {
); );
} }
testWidgets('renders header, pinned current track, and upcoming rows', (tester) async { testWidgets('renders header and the full queue including history', (tester) async {
final service = _FakeQueueService([_track('t1', 'Alpha'), _track('t2', 'Beta'), _track('t3', 'Gamma')]); final service = _FakeQueueService([_track('t1', 'Alpha'), _track('t2', 'Beta'), _track('t3', 'Gamma')]);
await tester.pumpWidget(wrap(service)); await tester.pumpWidget(wrap(service));
@@ -100,17 +101,14 @@ void main() {
expect(find.text(t.music.queue), findsOneWidget); expect(find.text(t.music.queue), findsOneWidget);
expect(find.text(t.music.trackCount(n: 3)), findsOneWidget); expect(find.text(t.music.trackCount(n: 3)), findsOneWidget);
// Pinned current row (not a TrackRow) + "Up next" label. // The whole queue renders as TrackRows: played, current, and upcoming.
expect(find.byType(TrackRow), findsNWidgets(3));
expect(find.text('Alpha'), findsOneWidget); expect(find.text('Alpha'), findsOneWidget);
expect(find.text(t.music.upNext), findsOneWidget);
// Upcoming tracks render as TrackRows.
expect(find.byType(TrackRow), findsNWidgets(2));
expect(find.text('Beta'), findsOneWidget); expect(find.text('Beta'), findsOneWidget);
expect(find.text('Gamma'), findsOneWidget); expect(find.text('Gamma'), findsOneWidget);
}); });
testWidgets('tapping an upcoming row jumps to its queue index', (tester) async { testWidgets('tapping a played or upcoming row jumps to its queue index', (tester) async {
final service = _FakeQueueService([_track('t1', 'Alpha'), _track('t2', 'Beta'), _track('t3', 'Gamma')]); final service = _FakeQueueService([_track('t1', 'Alpha'), _track('t2', 'Beta'), _track('t3', 'Gamma')]);
await tester.pumpWidget(wrap(service)); await tester.pumpWidget(wrap(service));
@@ -121,7 +119,9 @@ void main() {
await tester.tap(find.text('Gamma')); await tester.tap(find.text('Gamma'));
await tester.pump(); await tester.pump();
await tester.tap(find.text('Alpha'));
await tester.pump();
expect(service.jumps, [2]); expect(service.jumps, [2, 0]);
}); });
} }