mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-04 05:38:12 +02:00
fix: clean up settings merge regressions
This commit is contained in:
@@ -651,9 +651,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
one: 'трек',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return 'Удалить $count $_temp0 из истории?\n\nЭто также удалит файлы из хранилища.';
|
||||
}
|
||||
@@ -706,9 +706,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
one: 'трек',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return 'Удалено $count $_temp0';
|
||||
}
|
||||
@@ -1176,9 +1176,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count треков',
|
||||
one: '1 трек',
|
||||
many: '$count треков',
|
||||
few: '$count трека',
|
||||
one: '$count трек',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1685,9 +1685,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
one: 'трек',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return 'Удалить $count $_temp0 из этого альбома?\n\nЭто также удалит файлы из хранилища.';
|
||||
}
|
||||
@@ -1709,9 +1709,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
one: 'трек',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return 'Удалить $count $_temp0';
|
||||
}
|
||||
@@ -1942,9 +1942,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
one: 'трек',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -2098,9 +2098,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count минут',
|
||||
one: '1 минуту',
|
||||
many: '$count минут',
|
||||
few: '$count минуты',
|
||||
one: '$count минуту',
|
||||
);
|
||||
return '$_temp0 назад';
|
||||
}
|
||||
@@ -2111,9 +2111,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count часов',
|
||||
one: '1 час',
|
||||
many: '$count часов',
|
||||
few: '$count часа',
|
||||
one: '$count час',
|
||||
);
|
||||
return '$_temp0 назад';
|
||||
}
|
||||
@@ -2598,9 +2598,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count треков',
|
||||
one: '1 трек',
|
||||
many: '$count треков',
|
||||
few: '$count трека',
|
||||
one: '$count трек',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -2751,9 +2751,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
one: 'трек',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return 'Отправить $count $_temp0';
|
||||
}
|
||||
@@ -2768,9 +2768,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'треков',
|
||||
one: 'трек',
|
||||
many: 'треков',
|
||||
few: 'трека',
|
||||
one: 'трек',
|
||||
);
|
||||
return 'Конвертировать $count $_temp0';
|
||||
}
|
||||
|
||||
+11
-11
@@ -796,7 +796,7 @@
|
||||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "Lösche {count} {count, plural, one {}=1{Track} other{Tracks}} aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.",
|
||||
"dialogDeleteSelectedMessage": "Lösche {count} {count, plural, =1{Track} other{Tracks}} aus dem Verlauf?\n\nDies löscht auch die Dateien aus dem Speicher.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
@@ -2133,7 +2133,7 @@
|
||||
"@downloadedAlbumDeleteSelected": {
|
||||
"description": "Button - delete selected tracks"
|
||||
},
|
||||
"downloadedAlbumDeleteMessage": "{count} {count, plural, one {}=1{Titel} other{Titel}} aus diesem Album löschen?\n\nDadurch werden auch die Dateien aus dem Speicher gelöscht.",
|
||||
"downloadedAlbumDeleteMessage": "{count} {count, plural, =1{Titel} other{Titel}} aus diesem Album löschen?\n\nDadurch werden auch die Dateien aus dem Speicher gelöscht.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"description": "Delete confirmation with count",
|
||||
"placeholders": {
|
||||
@@ -2159,7 +2159,7 @@
|
||||
"@downloadedAlbumTapToSelect": {
|
||||
"description": "Selection hint"
|
||||
},
|
||||
"downloadedAlbumDeleteCount": "Lösche {count} {count, plural, one {}=1{Titel}other{Titel}}",
|
||||
"downloadedAlbumDeleteCount": "Lösche {count} {count, plural, =1{Titel}other{Titel}}",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"description": "Delete button text with count",
|
||||
"placeholders": {
|
||||
@@ -2674,7 +2674,7 @@
|
||||
"@timeJustNow": {
|
||||
"description": "Relative time - less than a minute ago"
|
||||
},
|
||||
"timeMinutesAgo": "{count, plural, one {}=1{vor 1 Minute} other{vor {count} Minuten}}",
|
||||
"timeMinutesAgo": "{count, plural, =1{vor 1 Minute} other{vor {count} Minuten}}",
|
||||
"@timeMinutesAgo": {
|
||||
"description": "Relative time - minutes ago",
|
||||
"placeholders": {
|
||||
@@ -2683,7 +2683,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeHoursAgo": "{count, plural, one {}=1{vor 1 Stunde} other{vor {count} Stunden}}",
|
||||
"timeHoursAgo": "{count, plural, =1{vor 1 Stunde} other{vor {count} Stunden}}",
|
||||
"@timeHoursAgo": {
|
||||
"description": "Relative time - hours ago",
|
||||
"placeholders": {
|
||||
@@ -3507,7 +3507,7 @@
|
||||
"@collectionPlaylistRemoveCover": {
|
||||
"description": "Bottom sheet action to remove custom cover image from a playlist"
|
||||
},
|
||||
"selectionShareCount": "Teile {count} {count, plural, one {}=1{Titel}other{Titel}}",
|
||||
"selectionShareCount": "Teile {count} {count, plural, =1{Titel}other{Titel}}",
|
||||
"@selectionShareCount": {
|
||||
"description": "Share button text with count in selection mode",
|
||||
"placeholders": {
|
||||
@@ -3520,7 +3520,7 @@
|
||||
"@selectionShareNoFiles": {
|
||||
"description": "Snackbar when no selected files exist on disk"
|
||||
},
|
||||
"selectionConvertCount": "Konvertiere {count} {count, plural, one {}=1{Titel}other{Titel}}",
|
||||
"selectionConvertCount": "Konvertiere {count} {count, plural, =1{Titel}other{Titel}}",
|
||||
"@selectionConvertCount": {
|
||||
"description": "Convert button text with count in selection mode",
|
||||
"placeholders": {
|
||||
@@ -3537,7 +3537,7 @@
|
||||
"@selectionBatchConvertConfirmTitle": {
|
||||
"description": "Confirmation dialog title for batch conversion"
|
||||
},
|
||||
"selectionBatchConvertConfirmMessage": "Konvertiere {count} {format} {count, plural, one {}=1{Titel} other{Titel}} zu {bitrate}?\n\nOriginaldateien werden nach der Konvertierung gelöscht.",
|
||||
"selectionBatchConvertConfirmMessage": "Konvertiere {count} {format} {count, plural, =1{Titel} other{Titel}} zu {bitrate}?\n\nOriginaldateien werden nach der Konvertierung gelöscht.",
|
||||
"@selectionBatchConvertConfirmMessage": {
|
||||
"description": "Confirmation dialog message for batch conversion",
|
||||
"placeholders": {
|
||||
@@ -3552,7 +3552,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selectionBatchConvertConfirmMessageLossless": "Konvertiere {count} {count, plural, one {}=1{Titel} other{Titel}} in {format}? (kein Qualitätsverlust)\n\nOriginaldateien werden nach der Konvertierung gelöscht.",
|
||||
"selectionBatchConvertConfirmMessageLossless": "Konvertiere {count} {count, plural, =1{Titel} other{Titel}} in {format}? (kein Qualitätsverlust)\n\nOriginaldateien werden nach der Konvertierung gelöscht.",
|
||||
"@selectionBatchConvertConfirmMessageLossless": {
|
||||
"description": "Confirmation dialog message for lossless batch conversion",
|
||||
"placeholders": {
|
||||
@@ -3962,7 +3962,7 @@
|
||||
"@cacheRefresh": {
|
||||
"description": "Tooltip for refresh button on cache management page"
|
||||
},
|
||||
"dialogDownloadPlaylistsMessage": "Lade {trackCount} {trackCount, plural, one {}=1{Titel} other{Titel}} von {playlistCount} {playlistCount, plural, one {}=1{Playlist} other{Playlists}}?",
|
||||
"dialogDownloadPlaylistsMessage": "Lade {trackCount} {trackCount, plural, =1{Titel} other{Titel}} von {playlistCount} {playlistCount, plural, =1{Playlist} other{Playlists}}?",
|
||||
"@dialogDownloadPlaylistsMessage": {
|
||||
"description": "Dialog message for bulk playlist download confirmation",
|
||||
"placeholders": {
|
||||
@@ -4590,4 +4590,4 @@
|
||||
"@downloadFallbackExtensionsSubtitle": {
|
||||
"description": "Subtitle for fallback extensions item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,7 +796,7 @@
|
||||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "¿Eliminar {count} {count, plural, one {}=1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"dialogDeleteSelectedMessage": "¿Eliminar {count} {count, plural, =1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
@@ -875,7 +875,7 @@
|
||||
"@snackbarCredentialsCleared": {
|
||||
"description": "Snackbar - Spotify credentials removed"
|
||||
},
|
||||
"snackbarDeletedTracks": "Eliminado {count} {count, plural, one {}=1{pista} other{pistas}}",
|
||||
"snackbarDeletedTracks": "Eliminado {count} {count, plural, =1{pista} other{pistas}}",
|
||||
"@snackbarDeletedTracks": {
|
||||
"description": "Snackbar - tracks deleted",
|
||||
"placeholders": {
|
||||
@@ -1490,7 +1490,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracksCount": "{count, plural, one {}=1{1 pista} other{{count} pistas}}",
|
||||
"tracksCount": "{count, plural, =1{1 pista} other{{count} pistas}}",
|
||||
"@tracksCount": {
|
||||
"description": "Track count display",
|
||||
"placeholders": {
|
||||
@@ -2133,7 +2133,7 @@
|
||||
"@downloadedAlbumDeleteSelected": {
|
||||
"description": "Button - delete selected tracks"
|
||||
},
|
||||
"downloadedAlbumDeleteMessage": "¿Eliminar {count} {count, plural, one {}=1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"downloadedAlbumDeleteMessage": "¿Eliminar {count} {count, plural, =1{pista} other{pistas}} del historial?\n\nEsto también eliminará los archivos del almacenamiento.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"description": "Delete confirmation with count",
|
||||
"placeholders": {
|
||||
@@ -2159,7 +2159,7 @@
|
||||
"@downloadedAlbumTapToSelect": {
|
||||
"description": "Selection hint"
|
||||
},
|
||||
"downloadedAlbumDeleteCount": "¡Eliminar {count} {count, plural, one {}=1{pista} other{pistas}}",
|
||||
"downloadedAlbumDeleteCount": "¡Eliminar {count} {count, plural, =1{pista} other{pistas}}",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"description": "Delete button text with count",
|
||||
"placeholders": {
|
||||
|
||||
@@ -796,7 +796,7 @@
|
||||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "Apagar {count} {count, plural, one {}=1{faixa} other{faixas}} do histórico?\n\nIsso também apagará os arquivos do armazenamento.",
|
||||
"dialogDeleteSelectedMessage": "Apagar {count} {count, plural, =1{faixa} other{faixas}} do histórico?\n\nIsso também apagará os arquivos do armazenamento.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
@@ -875,7 +875,7 @@
|
||||
"@snackbarCredentialsCleared": {
|
||||
"description": "Snackbar - Spotify credentials removed"
|
||||
},
|
||||
"snackbarDeletedTracks": "{count} {count, plural, one {}=1{faixa apagada} other{faixas apagadas}}",
|
||||
"snackbarDeletedTracks": "{count} {count, plural, =1{faixa apagada} other{faixas apagadas}}",
|
||||
"@snackbarDeletedTracks": {
|
||||
"description": "Snackbar - tracks deleted",
|
||||
"placeholders": {
|
||||
@@ -1490,7 +1490,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracksCount": "{count, plural, one {}=1{1 faixa} other{{count} faixas}}",
|
||||
"tracksCount": "{count, plural, =1{1 faixa} other{{count} faixas}}",
|
||||
"@tracksCount": {
|
||||
"description": "Track count display",
|
||||
"placeholders": {
|
||||
@@ -2133,7 +2133,7 @@
|
||||
"@downloadedAlbumDeleteSelected": {
|
||||
"description": "Button - delete selected tracks"
|
||||
},
|
||||
"downloadedAlbumDeleteMessage": "Excluir {count} {count, plural, one {}=1{faixa} other{faixas}} deste álbum?\n\nIsso também excluirá os arquivos do armazenamento.",
|
||||
"downloadedAlbumDeleteMessage": "Excluir {count} {count, plural, =1{faixa} other{faixas}} deste álbum?\n\nIsso também excluirá os arquivos do armazenamento.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"description": "Delete confirmation with count",
|
||||
"placeholders": {
|
||||
@@ -2159,7 +2159,7 @@
|
||||
"@downloadedAlbumTapToSelect": {
|
||||
"description": "Selection hint"
|
||||
},
|
||||
"downloadedAlbumDeleteCount": "Apagar {count} {count, plural, one {}=1{faixa} other{faixas}}",
|
||||
"downloadedAlbumDeleteCount": "Apagar {count} {count, plural, =1{faixa} other{faixas}}",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"description": "Delete button text with count",
|
||||
"placeholders": {
|
||||
|
||||
+12
-12
@@ -796,7 +796,7 @@
|
||||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}} из истории?\n\nЭто также удалит файлы из хранилища.",
|
||||
"dialogDeleteSelectedMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}} из истории?\n\nЭто также удалит файлы из хранилища.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
@@ -875,7 +875,7 @@
|
||||
"@snackbarCredentialsCleared": {
|
||||
"description": "Snackbar - Spotify credentials removed"
|
||||
},
|
||||
"snackbarDeletedTracks": "Удалено {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}",
|
||||
"snackbarDeletedTracks": "Удалено {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
|
||||
"@snackbarDeletedTracks": {
|
||||
"description": "Snackbar - tracks deleted",
|
||||
"placeholders": {
|
||||
@@ -1490,7 +1490,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracksCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}}",
|
||||
"tracksCount": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}}",
|
||||
"@tracksCount": {
|
||||
"description": "Track count display",
|
||||
"placeholders": {
|
||||
@@ -2133,7 +2133,7 @@
|
||||
"@downloadedAlbumDeleteSelected": {
|
||||
"description": "Button - delete selected tracks"
|
||||
},
|
||||
"downloadedAlbumDeleteMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}} из этого альбома?\n\nЭто также удалит файлы из хранилища.",
|
||||
"downloadedAlbumDeleteMessage": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}} из этого альбома?\n\nЭто также удалит файлы из хранилища.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"description": "Delete confirmation with count",
|
||||
"placeholders": {
|
||||
@@ -2159,7 +2159,7 @@
|
||||
"@downloadedAlbumTapToSelect": {
|
||||
"description": "Selection hint"
|
||||
},
|
||||
"downloadedAlbumDeleteCount": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}",
|
||||
"downloadedAlbumDeleteCount": "Удалить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"description": "Delete button text with count",
|
||||
"placeholders": {
|
||||
@@ -2482,7 +2482,7 @@
|
||||
"@libraryAboutDescription": {
|
||||
"description": "Description of local library feature"
|
||||
},
|
||||
"libraryTracksUnit": "{count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}",
|
||||
"libraryTracksUnit": "{count, plural, one {трек} few {трека} many {треков} other{треков}}",
|
||||
"@libraryTracksUnit": {
|
||||
"description": "Unit label for tracks count (without the number itself)",
|
||||
"placeholders": {
|
||||
@@ -2674,7 +2674,7 @@
|
||||
"@timeJustNow": {
|
||||
"description": "Relative time - less than a minute ago"
|
||||
},
|
||||
"timeMinutesAgo": "{count, plural, one {{count} минуту} few {{count} минуты} many {{count} минут} =1 {1 минуту} other {{count} минут}} назад",
|
||||
"timeMinutesAgo": "{count, plural, one {{count} минуту} few {{count} минуты} many {{count} минут} other {{count} минут}} назад",
|
||||
"@timeMinutesAgo": {
|
||||
"description": "Relative time - minutes ago",
|
||||
"placeholders": {
|
||||
@@ -2683,7 +2683,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeHoursAgo": "{count, plural, one {{count} час} few {{count} часа} many {{count} часов} =1 {1 час} other {{count} часов}} назад",
|
||||
"timeHoursAgo": "{count, plural, one {{count} час} few {{count} часа} many {{count} часов} other {{count} часов}} назад",
|
||||
"@timeHoursAgo": {
|
||||
"description": "Relative time - hours ago",
|
||||
"placeholders": {
|
||||
@@ -3342,7 +3342,7 @@
|
||||
"@collectionNoPlaylistsSubtitle": {
|
||||
"description": "Empty state subtitle when user has no playlists"
|
||||
},
|
||||
"collectionPlaylistTracks": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} =1 {1 трек} other {{count} треков}}",
|
||||
"collectionPlaylistTracks": "{count, plural, one {{count} трек} few {{count} трека} many {{count} треков} other {{count} треков}}",
|
||||
"@collectionPlaylistTracks": {
|
||||
"description": "Track count label for custom playlists",
|
||||
"placeholders": {
|
||||
@@ -3507,7 +3507,7 @@
|
||||
"@collectionPlaylistRemoveCover": {
|
||||
"description": "Bottom sheet action to remove custom cover image from a playlist"
|
||||
},
|
||||
"selectionShareCount": "Отправить {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}",
|
||||
"selectionShareCount": "Отправить {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
|
||||
"@selectionShareCount": {
|
||||
"description": "Share button text with count in selection mode",
|
||||
"placeholders": {
|
||||
@@ -3520,7 +3520,7 @@
|
||||
"@selectionShareNoFiles": {
|
||||
"description": "Snackbar when no selected files exist on disk"
|
||||
},
|
||||
"selectionConvertCount": "Конвертировать {count} {count, plural, one {трек} few {трека} many {треков} =1{трек} other{треков}}",
|
||||
"selectionConvertCount": "Конвертировать {count} {count, plural, one {трек} few {трека} many {треков} other{треков}}",
|
||||
"@selectionConvertCount": {
|
||||
"description": "Convert button text with count in selection mode",
|
||||
"placeholders": {
|
||||
@@ -4590,4 +4590,4 @@
|
||||
"@downloadFallbackExtensionsSubtitle": {
|
||||
"description": "Subtitle for fallback extensions item"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,7 +736,7 @@
|
||||
"@dialogDeleteSelectedTitle": {
|
||||
"description": "Dialog title - delete selected items"
|
||||
},
|
||||
"dialogDeleteSelectedMessage": "{count} {count, plural, one {}=1{şarkıyı} other{şarkıyı}} geçmişten silmeye emin misiniz?\n\nBu işlem seçilenleri cihazınızdan da silecektir.",
|
||||
"dialogDeleteSelectedMessage": "{count} {count, plural, =1{şarkıyı} other{şarkıyı}} geçmişten silmeye emin misiniz?\n\nBu işlem seçilenleri cihazınızdan da silecektir.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"description": "Dialog message - delete selected tracks",
|
||||
"placeholders": {
|
||||
@@ -815,7 +815,7 @@
|
||||
"@snackbarCredentialsCleared": {
|
||||
"description": "Snackbar - Spotify credentials removed"
|
||||
},
|
||||
"snackbarDeletedTracks": "{count} {count, plural, one {}=1{şarkı} other{şarkı}} silindi",
|
||||
"snackbarDeletedTracks": "{count} {count, plural, =1{şarkı} other{şarkı}} silindi",
|
||||
"@snackbarDeletedTracks": {
|
||||
"description": "Snackbar - tracks deleted",
|
||||
"placeholders": {
|
||||
@@ -1418,7 +1418,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tracksCount": "{count, plural, one {}=1{1 parça} other{{count} parça}}",
|
||||
"tracksCount": "{count, plural, =1{1 parça} other{{count} parça}}",
|
||||
"@tracksCount": {
|
||||
"description": "Track count display",
|
||||
"placeholders": {
|
||||
@@ -2045,7 +2045,7 @@
|
||||
"@downloadedAlbumDeleteSelected": {
|
||||
"description": "Button - delete selected tracks"
|
||||
},
|
||||
"downloadedAlbumDeleteMessage": "Bu albümden {count} {count, plural, one {}=1{parça} other{parça}} parça silinsin mi?\n\nBu işlem dosyaları depolama alanından da kalıcı olarak silecektir.",
|
||||
"downloadedAlbumDeleteMessage": "Bu albümden {count} {count, plural, =1{parça} other{parça}} parça silinsin mi?\n\nBu işlem dosyaları depolama alanından da kalıcı olarak silecektir.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"description": "Delete confirmation with count",
|
||||
"placeholders": {
|
||||
@@ -2071,7 +2071,7 @@
|
||||
"@downloadedAlbumTapToSelect": {
|
||||
"description": "Selection hint"
|
||||
},
|
||||
"downloadedAlbumDeleteCount": "{count} {count, plural, one {}=1{parçayı} other{parçayı}} sil",
|
||||
"downloadedAlbumDeleteCount": "{count} {count, plural, =1{parçayı} other{parçayı}} sil",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"description": "Delete button text with count",
|
||||
"placeholders": {
|
||||
|
||||
@@ -14,6 +14,7 @@ const _settingsKey = 'app_settings';
|
||||
const _migrationVersionKey = 'settings_migration_version';
|
||||
const _currentMigrationVersion = 11;
|
||||
const _spotifyClientSecretKey = 'spotify_client_secret';
|
||||
const _retiredBuiltInProviderIds = {'deezer', 'qobuz', 'tidal', 'youtube'};
|
||||
final _log = AppLogger('SettingsProvider');
|
||||
|
||||
class SettingsNotifier extends Notifier<AppSettings> {
|
||||
@@ -23,6 +24,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
'track',
|
||||
'artist',
|
||||
'album',
|
||||
'playlist',
|
||||
};
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
@@ -51,6 +53,12 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
final sanitizedDefaultSearchTab = _normalizeDefaultSearchTab(
|
||||
loaded.defaultSearchTab,
|
||||
);
|
||||
final sanitizedDefaultService = _sanitizeRetiredBuiltInProviderId(
|
||||
loaded.defaultService,
|
||||
);
|
||||
final sanitizedSearchProvider = _sanitizeRetiredBuiltInProviderId(
|
||||
loaded.searchProvider,
|
||||
);
|
||||
state = loaded.copyWith(
|
||||
useExtensionProviders: true,
|
||||
downloadFallbackExtensionIds: sanitizedDownloadFallbackExtensionIds,
|
||||
@@ -58,6 +66,10 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
loaded.downloadFallbackExtensionIds != null &&
|
||||
sanitizedDownloadFallbackExtensionIds == null,
|
||||
defaultSearchTab: sanitizedDefaultSearchTab,
|
||||
defaultService: sanitizedDefaultService ?? '',
|
||||
searchProvider: sanitizedSearchProvider,
|
||||
clearSearchProvider:
|
||||
loaded.searchProvider != null && sanitizedSearchProvider == null,
|
||||
);
|
||||
|
||||
await _runMigrations(prefs);
|
||||
@@ -145,9 +157,20 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
state = state.copyWith(lastSeenVersion: AppInfo.version);
|
||||
// Migration 7/11: retired built-in services no longer fall back to a
|
||||
// preinstalled provider.
|
||||
if (state.defaultService == 'youtube' ||
|
||||
state.defaultService == 'deezer') {
|
||||
state = state.copyWith(defaultService: '');
|
||||
final sanitizedDefaultService = _sanitizeRetiredBuiltInProviderId(
|
||||
state.defaultService,
|
||||
);
|
||||
final sanitizedSearchProvider = _sanitizeRetiredBuiltInProviderId(
|
||||
state.searchProvider,
|
||||
);
|
||||
if (sanitizedDefaultService != state.defaultService ||
|
||||
sanitizedSearchProvider != state.searchProvider) {
|
||||
state = state.copyWith(
|
||||
defaultService: sanitizedDefaultService ?? '',
|
||||
searchProvider: sanitizedSearchProvider,
|
||||
clearSearchProvider:
|
||||
state.searchProvider != null && sanitizedSearchProvider == null,
|
||||
);
|
||||
}
|
||||
if (!state.useExtensionProviders) {
|
||||
state = state.copyWith(useExtensionProviders: true);
|
||||
@@ -211,6 +234,12 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
String? _sanitizeRetiredBuiltInProviderId(String? providerId) {
|
||||
final normalized = providerId?.trim().toLowerCase();
|
||||
if (normalized == null || normalized.isEmpty) return providerId;
|
||||
return _retiredBuiltInProviderIds.contains(normalized) ? null : providerId;
|
||||
}
|
||||
|
||||
Future<void> _normalizeSongLinkRegionIfNeeded() async {
|
||||
final normalized = _normalizeSongLinkRegion(state.songLinkRegion);
|
||||
if (normalized == state.songLinkRegion) return;
|
||||
@@ -244,7 +273,9 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
}
|
||||
|
||||
void setDefaultService(String service) {
|
||||
state = state.copyWith(defaultService: service);
|
||||
state = state.copyWith(
|
||||
defaultService: _sanitizeRetiredBuiltInProviderId(service) ?? '',
|
||||
);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
@@ -430,10 +461,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
}
|
||||
|
||||
void setSearchProvider(String? provider) {
|
||||
if (provider == null || provider.isEmpty) {
|
||||
final sanitized = _sanitizeRetiredBuiltInProviderId(provider);
|
||||
if (sanitized == null || sanitized.isEmpty) {
|
||||
state = state.copyWith(clearSearchProvider: true);
|
||||
} else {
|
||||
state = state.copyWith(searchProvider: provider);
|
||||
state = state.copyWith(searchProvider: sanitized);
|
||||
}
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
@@ -429,6 +429,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
'track' => 'track',
|
||||
'artist' => 'artist',
|
||||
'album' => 'album',
|
||||
'playlist' => 'playlist',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,19 +16,16 @@ class DownloadSettingsPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
static const _builtInServices = ['tidal', 'qobuz'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final extensionState = ref.watch(extensionProvider);
|
||||
final hasExtensions = extensionState.extensions.isNotEmpty;
|
||||
final hasDownloadExtensions = extensionState.extensions.any(
|
||||
(extension) => extension.enabled && extension.hasDownloadProvider,
|
||||
);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final topPadding = normalizedHeaderTopPadding(context);
|
||||
|
||||
final isBuiltInService = _builtInServices.contains(settings.defaultService);
|
||||
final isTidalService = settings.defaultService == 'tidal';
|
||||
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
child: Scaffold(
|
||||
@@ -103,94 +100,16 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.tune,
|
||||
title: context.l10n.downloadAskBeforeDownload,
|
||||
subtitle: isBuiltInService
|
||||
subtitle: hasDownloadExtensions
|
||||
? context.l10n.downloadAskQualitySubtitle
|
||||
: context.l10n.downloadSelectServiceToEnable,
|
||||
: context.l10n.extensionsNoDownloadProvider,
|
||||
value: settings.askQualityBeforeDownload,
|
||||
enabled: isBuiltInService,
|
||||
enabled: hasDownloadExtensions,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAskQualityBeforeDownload(value),
|
||||
showDivider: false,
|
||||
),
|
||||
if (!settings.askQualityBeforeDownload &&
|
||||
isBuiltInService) ...[
|
||||
_QualityOption(
|
||||
title: context.l10n.qualityFlacLossless,
|
||||
subtitle: context.l10n.qualityFlacLosslessSubtitle,
|
||||
isSelected: settings.audioQuality == 'LOSSLESS',
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAudioQuality('LOSSLESS'),
|
||||
),
|
||||
_QualityOption(
|
||||
title: context.l10n.qualityHiResFlac,
|
||||
subtitle: context.l10n.qualityHiResFlacSubtitle,
|
||||
isSelected: settings.audioQuality == 'HI_RES',
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAudioQuality('HI_RES'),
|
||||
),
|
||||
_QualityOption(
|
||||
title: context.l10n.qualityHiResFlacMax,
|
||||
subtitle: context.l10n.qualityHiResFlacMaxSubtitle,
|
||||
isSelected: settings.audioQuality == 'HI_RES_LOSSLESS',
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAudioQuality('HI_RES_LOSSLESS'),
|
||||
showDivider: isTidalService,
|
||||
),
|
||||
if (isTidalService)
|
||||
_QualityOption(
|
||||
title: context.l10n.downloadLossy320,
|
||||
subtitle: _getTidalHighFormatLabel(
|
||||
context,
|
||||
settings.tidalHighFormat,
|
||||
),
|
||||
isSelected: settings.audioQuality == 'HIGH',
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAudioQuality('HIGH'),
|
||||
showDivider: false,
|
||||
),
|
||||
if (isTidalService && settings.audioQuality == 'HIGH')
|
||||
SettingsItem(
|
||||
icon: Icons.tune,
|
||||
title: context.l10n.downloadLossyFormat,
|
||||
subtitle: _getTidalHighFormatLabel(
|
||||
context,
|
||||
settings.tidalHighFormat,
|
||||
),
|
||||
onTap: () => _showTidalHighFormatPicker(
|
||||
context,
|
||||
ref,
|
||||
settings.tidalHighFormat,
|
||||
),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
if (!isBuiltInService)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.downloadSelectTidalQobuz,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -257,18 +176,6 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
onChanged: (v) =>
|
||||
ref.read(settingsProvider.notifier).setAutoFallback(v),
|
||||
),
|
||||
if (hasExtensions)
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.extension,
|
||||
title: context.l10n.optionsUseExtensionProviders,
|
||||
subtitle: settings.useExtensionProviders
|
||||
? context.l10n.optionsUseExtensionProvidersOn
|
||||
: context.l10n.optionsUseExtensionProvidersOff,
|
||||
value: settings.useExtensionProviders,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setUseExtensionProviders(v),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.extension_outlined,
|
||||
title: context.l10n.downloadFallbackExtensions,
|
||||
@@ -323,19 +230,6 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
String _getTidalHighFormatLabel(BuildContext context, String format) {
|
||||
switch (format) {
|
||||
case 'mp3_320':
|
||||
return context.l10n.downloadLossyMp3;
|
||||
case 'opus_256':
|
||||
return context.l10n.downloadLossyOpus256;
|
||||
case 'opus_128':
|
||||
return context.l10n.downloadLossyOpus128;
|
||||
default:
|
||||
return context.l10n.downloadLossyMp3;
|
||||
}
|
||||
}
|
||||
|
||||
String _getSongLinkRegionLabel(String code) {
|
||||
const names = <String, String>{
|
||||
'US': 'United States',
|
||||
@@ -358,91 +252,6 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
return name == null ? effective : '$effective - $name';
|
||||
}
|
||||
|
||||
void _showTidalHighFormatPicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String current,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.downloadLossy320Format,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
context.l10n.downloadLossy320FormatDesc,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.audiotrack),
|
||||
title: Text(context.l10n.downloadLossyMp3),
|
||||
subtitle: Text(context.l10n.downloadLossyMp3Subtitle),
|
||||
trailing: current == 'mp3_320'
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setTidalHighFormat('mp3_320');
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.graphic_eq),
|
||||
title: Text(context.l10n.downloadLossyOpus256),
|
||||
subtitle: Text(context.l10n.downloadLossyOpus256Subtitle),
|
||||
trailing: current == 'opus_256'
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setTidalHighFormat('opus_256');
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.graphic_eq),
|
||||
title: Text(context.l10n.downloadLossyOpus128),
|
||||
subtitle: Text(context.l10n.downloadLossyOpus128Subtitle),
|
||||
trailing: current == 'opus_128'
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setTidalHighFormat('opus_128');
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNetworkModePicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
@@ -795,63 +604,61 @@ class _ServiceSelector extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final extState = ref.watch(extensionProvider);
|
||||
final builtInServiceIds = ['tidal', 'qobuz'];
|
||||
|
||||
final extensionProviders = extState.extensions
|
||||
.where((e) => e.enabled && e.hasDownloadProvider)
|
||||
.toList();
|
||||
|
||||
final isExtensionService = !builtInServiceIds.contains(currentService);
|
||||
final isCurrentExtensionEnabled = isExtensionService
|
||||
? extensionProviders.any((e) => e.id == currentService)
|
||||
: true;
|
||||
|
||||
final effectiveService = isCurrentExtensionEnabled ? currentService : '';
|
||||
final effectiveService =
|
||||
extensionProviders.any((extension) => extension.id == currentService)
|
||||
? currentService
|
||||
: '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ServiceChip(
|
||||
icon: Icons.music_note,
|
||||
label: 'Tidal',
|
||||
isSelected: effectiveService == 'tidal',
|
||||
onTap: () => onChanged('tidal'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _ServiceChip(
|
||||
icon: Icons.album,
|
||||
label: 'Qobuz',
|
||||
isSelected: effectiveService == 'qobuz',
|
||||
onTap: () => onChanged('qobuz'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (extensionProviders.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
child: extensionProviders.isEmpty
|
||||
? Row(
|
||||
children: [
|
||||
for (final extension in extensionProviders)
|
||||
_ServiceChip(
|
||||
icon: Icons.extension,
|
||||
label: extension.displayName,
|
||||
isSelected: effectiveService == extension.id,
|
||||
onTap: () => onChanged(extension.id),
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 18,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.extensionsNoDownloadProvider,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const spacing = 8.0;
|
||||
final chipWidth = (constraints.maxWidth - spacing) / 2;
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: [
|
||||
for (final extension in extensionProviders)
|
||||
SizedBox(
|
||||
width: chipWidth,
|
||||
child: _ServiceChip(
|
||||
icon: Icons.extension,
|
||||
label: extension.displayName,
|
||||
isSelected: effectiveService == extension.id,
|
||||
onTap: () => onChanged(extension.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -914,67 +721,6 @@ class _ServiceChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _QualityOption extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final bool showDivider;
|
||||
const _QualityOption({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
this.showDivider = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.bodyLarge),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isSelected
|
||||
? Icon(Icons.check_circle, color: colorScheme.primary)
|
||||
: Icon(Icons.circle_outlined, color: colorScheme.outline),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showDivider)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
indent: 20,
|
||||
endIndent: 20,
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConcurrentDownloadsItem extends StatelessWidget {
|
||||
final int currentValue;
|
||||
final ValueChanged<int> onChanged;
|
||||
@@ -1112,8 +858,6 @@ class _ConcurrentChip extends StatelessWidget {
|
||||
class _MetadataSourceSelector extends ConsumerWidget {
|
||||
const _MetadataSourceSelector();
|
||||
|
||||
static const _builtInProviders = {'tidal': 'Tidal', 'qobuz': 'Qobuz'};
|
||||
|
||||
Extension? _defaultSearchExtension(List<Extension> extensions) {
|
||||
return extensions
|
||||
.where(
|
||||
@@ -1135,34 +879,29 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
final extState = ref.watch(extensionProvider);
|
||||
|
||||
final rawSearchProvider = settings.searchProvider?.trim() ?? '';
|
||||
final isValidBuiltIn = _builtInProviders.containsKey(rawSearchProvider);
|
||||
final primarySearchExtension = _defaultSearchExtension(extState.extensions);
|
||||
final defaultProviderTarget =
|
||||
primarySearchExtension?.displayName ?? 'Tidal';
|
||||
primarySearchExtension?.displayName ??
|
||||
context.l10n.extensionsNoCustomSearch;
|
||||
final defaultProviderLabel =
|
||||
'${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)';
|
||||
final searchProvider =
|
||||
isValidBuiltIn ||
|
||||
extState.extensions.any(
|
||||
(e) =>
|
||||
e.enabled && e.hasCustomSearch && e.id == rawSearchProvider,
|
||||
)
|
||||
extState.extensions.any(
|
||||
(e) => e.enabled && e.hasCustomSearch && e.id == rawSearchProvider,
|
||||
)
|
||||
? rawSearchProvider
|
||||
: '';
|
||||
final isBuiltIn = _builtInProviders.containsKey(searchProvider);
|
||||
|
||||
Extension? activeExtension;
|
||||
if (searchProvider.isNotEmpty && !isBuiltIn) {
|
||||
if (searchProvider.isNotEmpty) {
|
||||
activeExtension = extState.extensions
|
||||
.where((e) => e.id == searchProvider && e.enabled)
|
||||
.firstOrNull;
|
||||
}
|
||||
final hasNonDefaultProvider = isBuiltIn || activeExtension != null;
|
||||
final hasNonDefaultProvider = activeExtension != null;
|
||||
|
||||
String subtitle;
|
||||
if (isBuiltIn) {
|
||||
subtitle = 'Using ${_builtInProviders[searchProvider]}';
|
||||
} else if (activeExtension != null) {
|
||||
if (activeExtension != null) {
|
||||
subtitle = context.l10n.optionsUsingExtension(
|
||||
activeExtension.displayName,
|
||||
);
|
||||
@@ -1191,28 +930,20 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
_SettingsChoiceGrid(
|
||||
children: [
|
||||
_SearchProviderChip(
|
||||
_SettingsChoiceChip(
|
||||
icon: Icons.auto_awesome,
|
||||
label: defaultProviderLabel,
|
||||
isSelected: searchProvider.isEmpty,
|
||||
onTap: () =>
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(''),
|
||||
),
|
||||
for (final entry in _builtInProviders.entries)
|
||||
_SearchProviderChip(
|
||||
label: entry.value,
|
||||
isSelected: searchProvider == entry.key,
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setSearchProvider(entry.key),
|
||||
),
|
||||
for (final ext in extState.extensions.where(
|
||||
(e) => e.enabled && e.hasCustomSearch,
|
||||
))
|
||||
_SearchProviderChip(
|
||||
_SettingsChoiceChip(
|
||||
icon: Icons.extension,
|
||||
label: ext.displayName,
|
||||
isSelected: searchProvider == ext.id,
|
||||
onTap: () => ref
|
||||
@@ -1227,11 +958,36 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchProviderChip extends StatelessWidget {
|
||||
class _SettingsChoiceGrid extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
const _SettingsChoiceGrid({required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const spacing = 8.0;
|
||||
final chipWidth = (constraints.maxWidth - spacing) / 2;
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: [
|
||||
for (final child in children)
|
||||
SizedBox(width: chipWidth, child: child),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsChoiceChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
const _SearchProviderChip({
|
||||
const _SettingsChoiceChip({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
@@ -1240,16 +996,51 @@ class _SearchProviderChip extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onTap(),
|
||||
selectedColor: colorScheme.primaryContainer,
|
||||
checkmarkColor: colorScheme.onPrimaryContainer,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurface,
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final unselectedColor = isDark
|
||||
? Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.05),
|
||||
colorScheme.surface,
|
||||
)
|
||||
: colorScheme.surfaceContainerHigh;
|
||||
return Material(
|
||||
color: isSelected ? colorScheme.primaryContainer : unselectedColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1258,21 +1049,67 @@ class _SearchProviderChip extends StatelessWidget {
|
||||
class _DefaultSearchTabSelector extends ConsumerWidget {
|
||||
const _DefaultSearchTabSelector();
|
||||
|
||||
String _labelForTab(BuildContext context, String tab) {
|
||||
return switch (tab) {
|
||||
'track' => context.l10n.searchTracks,
|
||||
'artist' => context.l10n.searchArtists,
|
||||
'album' => context.l10n.searchAlbums,
|
||||
'playlist' => context.l10n.searchPlaylists,
|
||||
_ => context.l10n.historyFilterAll,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(settingsProvider);
|
||||
return SettingsItem(
|
||||
icon: Icons.tab_outlined,
|
||||
title: context.l10n.optionsDefaultSearchTab,
|
||||
subtitle: settings.defaultSearchTab == 'albums'
|
||||
? context.l10n.optionsDefaultSearchTabAlbums
|
||||
: context.l10n.optionsDefaultSearchTabTracks,
|
||||
onTap: () {
|
||||
final current = settings.defaultSearchTab;
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDefaultSearchTab(current == 'albums' ? 'tracks' : 'albums');
|
||||
},
|
||||
final current = settings.defaultSearchTab;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.optionsDefaultSearchTab,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.optionsDefaultSearchTabSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_SettingsChoiceGrid(
|
||||
children: [
|
||||
for (final tab in const [
|
||||
'all',
|
||||
'track',
|
||||
'artist',
|
||||
'album',
|
||||
'playlist',
|
||||
])
|
||||
_SettingsChoiceChip(
|
||||
icon: switch (tab) {
|
||||
'track' => Icons.music_note,
|
||||
'artist' => Icons.person,
|
||||
'album' => Icons.album,
|
||||
'playlist' => Icons.queue_music,
|
||||
_ => Icons.grid_view,
|
||||
},
|
||||
label: _labelForTab(context, tab),
|
||||
isSelected: current == tab,
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDefaultSearchTab(tab),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,10 @@ class MetadataSettingsPage extends ConsumerWidget {
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.sell_outlined,
|
||||
title: 'Embed Metadata',
|
||||
title: context.l10n.optionsEmbedMetadata,
|
||||
subtitle: settings.embedMetadata
|
||||
? 'Write metadata, cover art, and lyrics to files'
|
||||
: 'Disabled (advanced): skip all metadata embedding',
|
||||
? context.l10n.optionsEmbedMetadataSubtitleOn
|
||||
: context.l10n.optionsEmbedMetadataSubtitleOff,
|
||||
value: settings.embedMetadata,
|
||||
onChanged: (v) =>
|
||||
ref.read(settingsProvider.notifier).setEmbedMetadata(v),
|
||||
|
||||
Reference in New Issue
Block a user