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:
@@ -975,7 +975,6 @@
|
|||||||
"nowPlaying": "Сега се възпроизвежда",
|
"nowPlaying": "Сега се възпроизвежда",
|
||||||
"playingFrom": "Възпроизвеждане от ${title}",
|
"playingFrom": "Възпроизвеждане от ${title}",
|
||||||
"queue": "Опашка",
|
"queue": "Опашка",
|
||||||
"upNext": "Следва",
|
|
||||||
"clearQueue": "Изчисти опашката",
|
"clearQueue": "Изчисти опашката",
|
||||||
"lyrics": "Текст",
|
"lyrics": "Текст",
|
||||||
"noLyrics": "Няма наличен текст",
|
"noLyrics": "Няма наличен текст",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -974,7 +974,6 @@
|
|||||||
"nowPlaying": "再生中",
|
"nowPlaying": "再生中",
|
||||||
"playingFrom": "${title} から再生",
|
"playingFrom": "${title} から再生",
|
||||||
"queue": "再生キュー",
|
"queue": "再生キュー",
|
||||||
"upNext": "次はこちら",
|
|
||||||
"clearQueue": "キューをクリア",
|
"clearQueue": "キューをクリア",
|
||||||
"lyrics": "歌詞",
|
"lyrics": "歌詞",
|
||||||
"noLyrics": "歌詞がありません",
|
"noLyrics": "歌詞がありません",
|
||||||
|
|||||||
@@ -974,7 +974,6 @@
|
|||||||
"nowPlaying": "재생 중",
|
"nowPlaying": "재생 중",
|
||||||
"playingFrom": "${title}에서 재생 중",
|
"playingFrom": "${title}에서 재생 중",
|
||||||
"queue": "재생 대기열",
|
"queue": "재생 대기열",
|
||||||
"upNext": "다음 항목",
|
|
||||||
"clearQueue": "대기열 비우기",
|
"clearQueue": "대기열 비우기",
|
||||||
"lyrics": "가사",
|
"lyrics": "가사",
|
||||||
"noLyrics": "가사 없음",
|
"noLyrics": "가사 없음",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -977,7 +977,6 @@
|
|||||||
"nowPlaying": "Сейчас играет",
|
"nowPlaying": "Сейчас играет",
|
||||||
"playingFrom": "Воспроизведение из ${title}",
|
"playingFrom": "Воспроизведение из ${title}",
|
||||||
"queue": "Очередь",
|
"queue": "Очередь",
|
||||||
"upNext": "Далее",
|
|
||||||
"clearQueue": "Очистить очередь",
|
"clearQueue": "Очистить очередь",
|
||||||
"lyrics": "Текст песни",
|
"lyrics": "Текст песни",
|
||||||
"noLyrics": "Текст песни недоступен",
|
"noLyrics": "Текст песни недоступен",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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 => 'Kø';
|
@override String get queue => 'Kø';
|
||||||
@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' => 'Kø',
|
'music.queue' => 'Kø',
|
||||||
'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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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 => 'Kø';
|
@override String get queue => 'Kø';
|
||||||
@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' => 'Kø',
|
'music.queue' => 'Kø',
|
||||||
'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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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 => 'Kö';
|
@override String get queue => 'Kö';
|
||||||
@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' => 'Kö',
|
'music.queue' => 'Kö',
|
||||||
'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}',
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -974,7 +974,6 @@
|
|||||||
"nowPlaying": "正在播放",
|
"nowPlaying": "正在播放",
|
||||||
"playingFrom": "来自 ${title}",
|
"playingFrom": "来自 ${title}",
|
||||||
"queue": "播放队列",
|
"queue": "播放队列",
|
||||||
"upNext": "即将播放",
|
|
||||||
"clearQueue": "清空队列",
|
"clearQueue": "清空队列",
|
||||||
"lyrics": "歌词",
|
"lyrics": "歌词",
|
||||||
"noLyrics": "暂无歌词",
|
"noLyrics": "暂无歌词",
|
||||||
|
|||||||
+139
-156
@@ -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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user