diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png index fd5e939d..872d3c35 100644 Binary files a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png index 23686cb5..d7cc37f1 100644 Binary files a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png index 093d5c26..a8b158b8 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png index 590c5cda..0c3289e2 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png index 1f540975..f4606d20 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index ee6d88cc..ab7a9c81 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index a21d95ae..e90f1174 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 382e5cbb..17c3bf6f 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index de7571e9..f96e994e 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 6f0aa29e..e0887594 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 288d7ffa..85037969 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 1967ef76..695000ba 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index fd15ff97..dbcabffb 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f5cb5270..76b29751 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index ca280234..90ccf8bc 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 85b3cd51..179ed04c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index e34cd643..b4b3e9a6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index fd15ff97..dbcabffb 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index bd83c86d..84c9f5d4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 4eadb79e..4d1f3ab6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png index eba0c613..44d09d07 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png index c0c2baac..376ee0b4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png index b99ad6d0..e082e352 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png index c4298a3b..6f1043d7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 4eadb79e..4d1f3ab6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index c4363ca4..d24c702a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png index b61e41d5..ab7a9c81 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index b656689e..f96e994e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index b18d03d1..cb2efa40 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index cbf1ca53..b66ecd68 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index fcc9e1ed..4c3c20e4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 188f5d34..33150a80 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -121,7 +121,7 @@ "@appearanceDynamicColor": { "description": "Material You dynamic colors" }, - "appearanceDynamicColorSubtitle": "Farben von Ihrem Hintergrundbild verwenden", + "appearanceDynamicColorSubtitle": "Farben deines Hintergrundbilds verwenden", "@appearanceDynamicColorSubtitle": { "description": "Subtitle for dynamic color" }, @@ -178,7 +178,7 @@ "@optionsAutoFallbackSubtitle": { "description": "Subtitle for auto fallback" }, - "optionsUseExtensionProviders": "Erweiterungs-Anbieter verwenden", + "optionsUseExtensionProviders": "Erweiterungsanbieter verwenden", "@optionsUseExtensionProviders": { "description": "Enable extension download providers" }, @@ -194,7 +194,7 @@ "@optionsEmbedLyrics": { "description": "Embed lyrics in audio files" }, - "optionsEmbedLyricsSubtitle": "Synchronisierte Lyrics in FLAC-Dateien einbetten", + "optionsEmbedLyricsSubtitle": "Synchronisierte Songtexte neben heruntergeladenen Titeln speichern", "@optionsEmbedLyricsSubtitle": { "description": "Subtitle for embed lyrics" }, @@ -230,7 +230,7 @@ "@optionsArtistTagModeJoined": { "description": "Artist tag mode option that joins multiple artists into one value" }, - "optionsArtistTagModeJoinedSubtitle": "Einen Künstler wert wie \"Artist A, Artist B\" für maximale Spieler-Kompatibilität schreiben.", + "optionsArtistTagModeJoinedSubtitle": "Einen Künstler-Wert wie \"Künstler A, Künstler B\" für maximale Player-Kompatibilität schreiben.", "@optionsArtistTagModeJoinedSubtitle": { "description": "Subtitle for joined artist tag mode" }, @@ -438,7 +438,7 @@ "@aboutReportIssue": { "description": "Link to report bugs" }, - "aboutReportIssueSubtitle": "Melde jedes Problem, die dir auftreten", + "aboutReportIssueSubtitle": "Melde Probleme, die dir auffallen", "@aboutReportIssueSubtitle": { "description": "Subtitle for report issue" }, @@ -486,7 +486,7 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, - "aboutSjdonadoDesc": "Ersteller von I Don't Have Spotify (IDHS). Der Fallback-Link-Resolver, der den Tag rettete!", + "aboutSjdonadoDesc": "Ersteller von I Don't Have Spotify (IDHS). Der Fallback-Link-Resolver, der den Tag rettet!", "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, @@ -576,7 +576,7 @@ "@setupPermissionDeniedMessage": { "description": "Error when permission denied" }, - "setupPermissionRequired": "{permissionType} Zugriff verweigert", + "setupPermissionRequired": "{permissionType}-Berechtigung erforderlich", "@setupPermissionRequired": { "description": "Generic permission required title", "placeholders": { @@ -586,7 +586,7 @@ } } }, - "setupPermissionRequiredMessage": "{permissionType} Berechtigung ist erforderlich für\ndie beste Benutzererfahrung. Für kannst dies später in den Einstellungen ändern.", + "setupPermissionRequiredMessage": "{permissionType}-Berechtigung ist erforderlich für\ndie beste Benutzererfahrung. Du kannst dies später in den Einstellungen ändern.", "@setupPermissionRequiredMessage": { "description": "Generic permission required message", "placeholders": { @@ -603,7 +603,7 @@ "@setupNoFolderSelected": { "description": "Prompt when no folder selected" }, - "setupUseDefault": "Standart benutzen", + "setupUseDefault": "Standard verwenden", "@setupUseDefault": { "description": "Button to use default folder" }, @@ -663,7 +663,7 @@ "@setupNotificationEnable": { "description": "Button to enable notifications" }, - "setupFolderChoose": "Speicherort auwählen", + "setupFolderChoose": "Speicherort auswählen", "@setupFolderChoose": { "description": "Button to choose folder" }, @@ -747,7 +747,7 @@ "@dialogDiscardChanges": { "description": "Dialog title - unsaved changes warning" }, - "dialogUnsavedChanges": "Hast du noch nicht alle Änderungen gespeichert. Möchtest du die Änderungen verwerfen?", + "dialogUnsavedChanges": "Du hast ungespeicherte Änderungen. Möchtest du sie verwerfen?", "@dialogUnsavedChanges": { "description": "Dialog message - unsaved changes" }, @@ -759,7 +759,7 @@ "@dialogRemoveExtension": { "description": "Dialog title - uninstall extension" }, - "dialogRemoveExtensionMessage": "Bist Du sicher, dass Du diese Erweiterung entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "dialogRemoveExtensionMessage": "Bist du sicher, dass du diese Erweiterung entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "@dialogRemoveExtensionMessage": { "description": "Dialog message - uninstall confirmation" }, @@ -767,7 +767,7 @@ "@dialogUninstallExtension": { "description": "Dialog title - uninstall extension" }, - "dialogUninstallExtensionMessage": "Bist du dir sicher, dass du {extensionName} entfernen möchtest?", + "dialogUninstallExtensionMessage": "Bist du sicher, dass du {extensionName} entfernen möchtest?", "@dialogUninstallExtensionMessage": { "description": "Dialog message - uninstall specific extension", "placeholders": { @@ -780,7 +780,7 @@ "@dialogClearHistoryTitle": { "description": "Dialog title - clear download history" }, - "dialogClearHistoryMessage": "Bist du dir sicher, dass du den gesamten Download verlauf löschen möchten? Dies kann nicht rückgängig gemacht werden.", + "dialogClearHistoryMessage": "Bist du sicher, dass du den gesamten Downloadverlauf löschen möchtest? Dies kann nicht rückgängig gemacht werden.", "@dialogClearHistoryMessage": { "description": "Dialog message - clear history confirmation" }, @@ -846,7 +846,7 @@ } } }, - "snackbarAlreadyInLibrary": "\"{trackName}\" existiert bereits in Ihrer Bibliothek", + "snackbarAlreadyInLibrary": "\"{trackName}\" existiert bereits in deiner Bibliothek", "@snackbarAlreadyInLibrary": { "description": "Snackbar - track already exists in local library", "placeholders": { @@ -867,7 +867,7 @@ "@snackbarCredentialsCleared": { "description": "Snackbar - Spotify credentials removed" }, - "snackbarDeletedTracks": "{count} {count, plural, one {}=1{Titel} other{Titel}}", + "snackbarDeletedTracks": "{count} {count, plural, =1{Titel gelöscht} other{Titel gelöscht}}", "@snackbarDeletedTracks": { "description": "Snackbar - tracks deleted", "placeholders": { @@ -969,7 +969,7 @@ "@errorUrlFetchFailed": { "description": "Error message - generic URL fetch failure" }, - "errorMissingExtensionSource": "Kann {item} nicht lade wegen fehlender Erweiterungsquelle", + "errorMissingExtensionSource": "Kann {item} nicht laden wegen fehlender Erweiterungsquelle", "@errorMissingExtensionSource": { "description": "Error - extension source not available", "placeholders": { @@ -1287,7 +1287,7 @@ "@logClearLogsTitle": { "description": "Clear logs dialog title" }, - "logClearLogsMessage": "Bist du dir sicher, dass Sie alle Protokolle löschen möchtest?", + "logClearLogsMessage": "Bist du sicher, dass du alle Protokolle löschen möchtest?", "@logClearLogsMessage": { "description": "Clear logs confirmation message" }, @@ -2462,7 +2462,7 @@ "@libraryClearConfirmTitle": { "description": "Dialog title for clear confirmation" }, - "libraryClearConfirmMessage": "Dadurch werden alle gescannten Titel aus Ihrer Bibliothek entfernt. Ihre eigentlichen Musikdateien werden nicht gelöscht.", + "libraryClearConfirmMessage": "Dadurch werden alle gescannten Titel aus deiner Bibliothek entfernt. Deine eigentlichen Musikdateien werden nicht gelöscht.", "@libraryClearConfirmMessage": { "description": "Dialog message for clear confirmation" }, @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e3f4a6d0..a5d4d5ac 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2026-01-16", + "@@last_modified": "2026-04-28", "appName": "SpotiFLAC", "@appName": { "description": "App name - DO NOT TRANSLATE" @@ -99,7 +99,7 @@ }, "downloadFolderOrganization": "Folder Organization", "@downloadFolderOrganization": { - "description": "Setting for folder structure" + "description": "Title of the folder organization picker bottom sheet" }, "appearanceTitle": "Appearance", "@appearanceTitle": { @@ -145,7 +145,7 @@ "@optionsPrimaryProvider": { "description": "Main search provider setting" }, - "optionsPrimaryProviderSubtitle": "Service used when searching by track name.", + "optionsPrimaryProviderSubtitle": "Service used for searching by track or album name", "@optionsPrimaryProviderSubtitle": { "description": "Subtitle for primary provider" }, @@ -194,7 +194,7 @@ "@optionsEmbedLyrics": { "description": "Embed lyrics in audio files" }, - "optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files", + "optionsEmbedLyricsSubtitle": "Save synced lyrics alongside your downloaded tracks", "@optionsEmbedLyricsSubtitle": { "description": "Subtitle for embed lyrics" }, @@ -729,7 +729,7 @@ }, "dialogDownload": "Download", "@dialogDownload": { - "description": "Dialog button - download action" + "description": "Confirm button in Download All dialog" }, "dialogDiscard": "Discard", "@dialogDiscard": { @@ -1449,11 +1449,11 @@ "@settingsAppearanceSubtitle": { "description": "Appearance settings description" }, - "settingsDownloadSubtitle": "Service, quality, filename format", + "settingsDownloadSubtitle": "Service, quality, fallback", "@settingsDownloadSubtitle": { "description": "Download settings description" }, - "settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates", + "settingsOptionsSubtitle": "Fallback, metadata, lyrics, cover art", "@settingsOptionsSubtitle": { "description": "Options settings description" }, @@ -3592,18 +3592,17 @@ } } }, - "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Artist folders use Album Artist when available", + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Folder named after Album Artist tag", "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { - "description": "Subtitle when Album Artist is used for folder naming" + "description": "Subtitle when album artist is used for folder names" }, - "downloadUseAlbumArtistForFoldersTrackSubtitle": "Artist folders use Track Artist only", + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Folder named after Track Artist tag", "@downloadUseAlbumArtistForFoldersTrackSubtitle": { - "description": "Subtitle when Track Artist is used for folder naming" + "description": "Subtitle when track artist is used for folder names" }, - - "lyricsProvidersTitle": "Lyrics Providers", + "lyricsProvidersTitle": "Lyrics Provider Priority", "@lyricsProvidersTitle": { - "description": "Title for the lyrics provider priority page" + "description": "Settings item title for lyrics provider order" }, "lyricsProvidersDescription": "Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.", "@lyricsProvidersDescription": { @@ -3667,7 +3666,6 @@ "@lyricsProviderExtensionDesc": { "description": "Generic description for extension-based lyrics providers" }, - "safMigrationTitle": "Storage Update Required", "@safMigrationTitle": { "description": "Title of SAF migration dialog" @@ -3684,16 +3682,14 @@ "@safMigrationSuccess": { "description": "Snackbar after successfully migrating to SAF" }, - - "settingsDonate": "Donate", + "settingsDonate": "Support Development", "@settingsDonate": { - "description": "Settings menu item - donate" + "description": "Settings menu item - donate page" }, - "settingsDonateSubtitle": "Support SpotiFLAC-Mobile development", + "settingsDonateSubtitle": "Buy the developer a coffee", "@settingsDonateSubtitle": { "description": "Subtitle for donate menu item" }, - "tooltipLoveAll": "Love All", "@tooltipLoveAll": { "description": "Tooltip for the Love All button on album/playlist screens" @@ -3720,10 +3716,9 @@ } } }, - "dialogDownloadAllTitle": "Download All", "@dialogDownloadAllTitle": { - "description": "Title of the Download All confirmation dialog" + "description": "Dialog title for bulk download confirmation" }, "dialogDownloadAllMessage": "Download {count} tracks?", "@dialogDownloadAllMessage": { @@ -3734,11 +3729,6 @@ } } }, - "dialogDownload": "Download", - "@dialogDownload": { - "description": "Confirm button in Download All dialog" - }, - "homeSkipAlreadyDownloaded": "Skip already downloaded songs", "@homeSkipAlreadyDownloaded": { "description": "Checkbox label in import dialog to skip already-downloaded songs" @@ -3751,7 +3741,6 @@ "@homeAlbumInfoUnavailable": { "description": "Snackbar when album info cannot be loaded" }, - "snackbarLoadingCueSheet": "Loading CUE sheet...", "@snackbarLoadingCueSheet": { "description": "Snackbar while loading a CUE sheet file" @@ -3781,190 +3770,181 @@ "@snackbarNoActionDefined": { "description": "Snackbar when an extension button has no action configured" }, - "noTracksFoundForAlbum": "No tracks found for this album", "@noTracksFoundForAlbum": { "description": "Empty state message when an album has no tracks" }, - - "downloadLocationSubtitle": "Choose storage mode for downloaded files.", + "downloadLocationSubtitle": "Choose where to save your downloaded tracks", "@downloadLocationSubtitle": { - "description": "Subtitle text in Android download location bottom sheet" + "description": "Subtitle shown in the download location picker sheet" }, - "storageModeAppFolder": "App folder (non-SAF)", + "storageModeAppFolder": "App Folder (Recommended)", "@storageModeAppFolder": { - "description": "Storage mode option - use legacy app folder" + "description": "Storage mode option - app-managed folder" }, - "storageModeAppFolderSubtitle": "Use default Music/SpotiFLAC path", + "storageModeAppFolderSubtitle": "Saves to Music/SpotiFLAC by default", "@storageModeAppFolderSubtitle": { "description": "Subtitle for app folder storage mode" }, - "storageModeSaf": "SAF folder", + "storageModeSaf": "Custom Folder (SAF)", "@storageModeSaf": { - "description": "Storage mode option - use Android SAF picker" + "description": "Storage mode option - Storage Access Framework" }, - "storageModeSafSubtitle": "Pick folder via Android Storage Access Framework", + "storageModeSafSubtitle": "Pick any folder, including SD card", "@storageModeSafSubtitle": { "description": "Subtitle for SAF storage mode" }, - "downloadFilenameDescription": "Customize how your files are named.", + "downloadFilenameDescription": "Use {artist}, {title}, {album}, {track}, {year}, {date}, {disc} as placeholders.", "@downloadFilenameDescription": { - "description": "Description text in filename format bottom sheet" + "description": "Description shown in filename format editor" }, "downloadFilenameInsertTag": "Tap to insert tag:", "@downloadFilenameInsertTag": { "description": "Label above filename tag chips" }, - "downloadSeparateSinglesEnabled": "Albums/ and Singles/ folders", + "downloadSeparateSinglesEnabled": "Singles and EPs saved in a separate folder", "@downloadSeparateSinglesEnabled": { - "description": "Subtitle when separate singles folder is enabled" + "description": "Subtitle when separate singles folder is on" }, - "downloadSeparateSinglesDisabled": "All files in same structure", + "downloadSeparateSinglesDisabled": "Singles and albums saved in the same folder", "@downloadSeparateSinglesDisabled": { - "description": "Subtitle when separate singles folder is disabled" + "description": "Subtitle when separate singles folder is off" }, "downloadArtistNameFilters": "Artist Name Filters", "@downloadArtistNameFilters": { "description": "Setting title for artist folder filter options" }, - "downloadCreatePlaylistSourceFolder": "Create playlist source folder", + "downloadCreatePlaylistSourceFolder": "Playlist Source Folder", "@downloadCreatePlaylistSourceFolder": { - "description": "Setting title for adding a playlist folder prefix before the normal organization structure" + "description": "Setting to create a subfolder per playlist source" }, - "downloadCreatePlaylistSourceFolderEnabled": "Playlist downloads use Playlist/ plus your normal folder structure.", + "downloadCreatePlaylistSourceFolderEnabled": "A subfolder is created for each playlist", "@downloadCreatePlaylistSourceFolderEnabled": { - "description": "Subtitle when playlist source folder prefix is enabled" + "description": "Subtitle when playlist folder is enabled" }, - "downloadCreatePlaylistSourceFolderDisabled": "Playlist downloads use the normal folder structure only.", + "downloadCreatePlaylistSourceFolderDisabled": "All tracks saved directly to download folder", "@downloadCreatePlaylistSourceFolderDisabled": { - "description": "Subtitle when playlist source folder prefix is disabled" + "description": "Subtitle when playlist folder is disabled" }, - "downloadCreatePlaylistSourceFolderRedundant": "By Playlist already places downloads inside a playlist folder.", + "downloadCreatePlaylistSourceFolderRedundant": "Handled by folder organization setting", "@downloadCreatePlaylistSourceFolderRedundant": { - "description": "Subtitle when playlist folder prefix setting is redundant because folder organization is already by playlist" + "description": "Subtitle when folder organization is already set to playlist" }, "downloadSongLinkRegion": "SongLink Region", "@downloadSongLinkRegion": { - "description": "Setting title for SongLink country region" + "description": "Setting for SongLink region used during fallback resolution" }, - "downloadNetworkCompatibilityMode": "Network compatibility mode", + "downloadNetworkCompatibilityMode": "Network Compatibility Mode", "@downloadNetworkCompatibilityMode": { - "description": "Setting title for network compatibility toggle" + "description": "Setting for legacy TLS/network handling" }, - "downloadNetworkCompatibilityModeEnabled": "Enabled: try HTTP + accept invalid TLS certificates (unsafe)", + "downloadNetworkCompatibilityModeEnabled": "Using legacy TLS settings for older networks", "@downloadNetworkCompatibilityModeEnabled": { - "description": "Subtitle when network compatibility mode is enabled" + "description": "Subtitle when network compatibility mode is on" }, - "downloadNetworkCompatibilityModeDisabled": "Off: strict HTTPS certificate validation (recommended)", + "downloadNetworkCompatibilityModeDisabled": "Using standard network settings", "@downloadNetworkCompatibilityModeDisabled": { - "description": "Subtitle when network compatibility mode is disabled" + "description": "Subtitle when network compatibility mode is off" }, - "downloadSelectServiceToEnable": "Select a built-in service to enable", + "downloadSelectServiceToEnable": "Select Tidal or Qobuz to enable this option", "@downloadSelectServiceToEnable": { - "description": "Hint shown instead of Ask-quality subtitle when no built-in service selected" + "description": "Subtitle when quality picker is disabled due to extension service" }, - - "downloadSelectTidalQobuz": "Select Tidal or Qobuz above to configure quality", + "downloadSelectTidalQobuz": "Select Tidal or Qobuz to choose audio quality", "@downloadSelectTidalQobuz": { - "description": "Info hint when non-Tidal/Qobuz service is selected" + "description": "Info shown when a non-built-in service is selected" }, - "downloadEmbedLyricsDisabled": "Disabled while Embed Metadata is turned off", + "downloadEmbedLyricsDisabled": "Enable metadata embedding first", "@downloadEmbedLyricsDisabled": { - "description": "Subtitle for Embed Lyrics when Embed Metadata is disabled" + "description": "Subtitle when lyrics embedding is blocked by metadata toggle" }, "downloadNeteaseIncludeTranslation": "Netease: Include Translation", "@downloadNeteaseIncludeTranslation": { - "description": "Toggle title for including Netease translated lyrics" + "description": "Setting to include translated lyrics from Netease" }, - "downloadNeteaseIncludeTranslationEnabled": "Append translated lyrics when available", + "downloadNeteaseIncludeTranslationEnabled": "Chinese translation lines included", "@downloadNeteaseIncludeTranslationEnabled": { - "description": "Subtitle when Netease translation is enabled" + "description": "Subtitle when Netease translation is on" }, - "downloadNeteaseIncludeTranslationDisabled": "Use original lyrics only", + "downloadNeteaseIncludeTranslationDisabled": "Original lyrics only", "@downloadNeteaseIncludeTranslationDisabled": { - "description": "Subtitle when Netease translation is disabled" + "description": "Subtitle when Netease translation is off" }, "downloadNeteaseIncludeRomanization": "Netease: Include Romanization", "@downloadNeteaseIncludeRomanization": { - "description": "Toggle title for including Netease romanized lyrics" + "description": "Setting to include romanized lyrics from Netease" }, - "downloadNeteaseIncludeRomanizationEnabled": "Append romanized lyrics when available", + "downloadNeteaseIncludeRomanizationEnabled": "Romanization lines included", "@downloadNeteaseIncludeRomanizationEnabled": { - "description": "Subtitle when Netease romanization is enabled" + "description": "Subtitle when Netease romanization is on" }, - "downloadNeteaseIncludeRomanizationDisabled": "Disabled", + "downloadNeteaseIncludeRomanizationDisabled": "No romanization", "@downloadNeteaseIncludeRomanizationDisabled": { - "description": "Subtitle when Netease romanization is disabled" + "description": "Subtitle when Netease romanization is off" }, - "downloadAppleQqMultiPerson": "Apple/QQ Multi-Person Word-by-Word", + "downloadAppleQqMultiPerson": "Apple / QQ: Multi-Person Lyrics", "@downloadAppleQqMultiPerson": { - "description": "Toggle title for Apple/QQ multi-person word-by-word lyrics" + "description": "Setting for word-by-word multi-person lyrics from Apple Music and QQ Music" }, - "downloadAppleQqMultiPersonEnabled": "Enable v1/v2 speaker and [bg:] tags", + "downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks", "@downloadAppleQqMultiPersonEnabled": { - "description": "Subtitle when multi-person word-by-word is enabled" + "description": "Subtitle when multi-person lyrics is on" }, - "downloadAppleQqMultiPersonDisabled": "Simplified word-by-word formatting", + "downloadAppleQqMultiPersonDisabled": "Standard lyrics without speaker labels", "@downloadAppleQqMultiPersonDisabled": { - "description": "Subtitle when multi-person word-by-word is disabled" + "description": "Subtitle when multi-person lyrics is off" }, "downloadMusixmatchLanguage": "Musixmatch Language", "@downloadMusixmatchLanguage": { - "description": "Setting title for Musixmatch language preference" + "description": "Setting for Musixmatch lyrics translation language" }, - "downloadMusixmatchLanguageAuto": "Auto (original)", + "downloadMusixmatchLanguageAuto": "Auto (original language)", "@downloadMusixmatchLanguageAuto": { - "description": "Option label when Musixmatch uses original language" + "description": "Subtitle when no language is set" }, - "downloadFilterContributing": "Filter contributing artists in Album Artist", + "downloadFilterContributing": "Filter Contributing Artists", "@downloadFilterContributing": { - "description": "Toggle title for filtering contributing artists in Album Artist metadata" + "description": "Setting to strip contributing artists from Album Artist folder name" }, - "downloadFilterContributingEnabled": "Album Artist metadata uses primary artist only", + "downloadFilterContributingEnabled": "Contributing artists removed from Album Artist folder name", "@downloadFilterContributingEnabled": { - "description": "Subtitle when contributing artist filter is enabled" + "description": "Subtitle when contributing artist filter is on" }, - "downloadFilterContributingDisabled": "Keep full Album Artist metadata value", + "downloadFilterContributingDisabled": "Full Album Artist string used", "@downloadFilterContributingDisabled": { - "description": "Subtitle when contributing artist filter is disabled" + "description": "Subtitle when contributing artist filter is off" }, - - "downloadProvidersNoneEnabled": "None enabled", + "downloadProvidersNoneEnabled": "No providers enabled", "@downloadProvidersNoneEnabled": { - "description": "Subtitle for lyrics providers setting when no providers are enabled" + "description": "Shown when no lyrics providers are active" }, "downloadMusixmatchLanguageCode": "Language code", "@downloadMusixmatchLanguageCode": { - "description": "Label for the Musixmatch language code text field" + "description": "Label for Musixmatch language input field" }, - "downloadMusixmatchLanguageHint": "auto / en / es / ja", + "downloadMusixmatchLanguageHint": "e.g. en, de, ja", "@downloadMusixmatchLanguageHint": { - "description": "Hint text for the Musixmatch language code field" + "description": "Placeholder for Musixmatch language input" }, - "downloadMusixmatchLanguageDesc": "Set preferred language code (example: en, es, ja). Leave empty for auto.", + "downloadMusixmatchLanguageDesc": "Enter a BCP-47 language code (e.g. en, de, ja) to request translated lyrics from Musixmatch.", "@downloadMusixmatchLanguageDesc": { - "description": "Description in the Musixmatch language picker" + "description": "Description in Musixmatch language picker" }, "downloadMusixmatchAuto": "Auto", "@downloadMusixmatchAuto": { - "description": "Button to reset Musixmatch language to automatic" + "description": "Button to clear Musixmatch language (use auto)" }, - - "downloadNetworkAnySubtitle": "WiFi + Mobile Data", + "downloadNetworkAnySubtitle": "Use WiFi or mobile data", "@downloadNetworkAnySubtitle": { - "description": "Subtitle for 'Any' network mode option" + "description": "Subtitle for any-network option in picker" }, - "downloadNetworkWifiOnlySubtitle": "Pause downloads on mobile data", + "downloadNetworkWifiOnlySubtitle": "Downloads pause when on mobile data", "@downloadNetworkWifiOnlySubtitle": { - "description": "Subtitle for 'WiFi only' network mode option" + "description": "Subtitle for WiFi-only option in picker" }, - "downloadSongLinkRegionDesc": "Used as userCountry for SongLink API lookup.", + "downloadSongLinkRegionDesc": "Region used when resolving track links via SongLink. Choose the country where your streaming services are available.", "@downloadSongLinkRegionDesc": { - "description": "Description in the SongLink region picker" - }, - "downloadFolderOrganization": "Folder Organization", - "@downloadFolderOrganization": { - "description": "Title of the folder organization picker bottom sheet" + "description": "Description in SongLink region picker" }, "snackbarUnsupportedAudioFormat": "Unsupported audio format", "@snackbarUnsupportedAudioFormat": { @@ -3974,10 +3954,6 @@ "@cacheRefresh": { "description": "Tooltip for refresh button on cache management page" }, - "dialogDownloadAllTitle": "Download All", - "@dialogDownloadAllTitle": { - "description": "Dialog title for bulk download confirmation" - }, "dialogDownloadPlaylistsMessage": "Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?", "@dialogDownloadPlaylistsMessage": { "description": "Dialog message for bulk playlist download confirmation", @@ -4105,7 +4081,6 @@ "@editMetadataSelectEmpty": { "description": "Button to select only fields that are currently empty" }, - "queueDownloadingCount": "Downloading ({count})", "@queueDownloadingCount": { "description": "Header for active downloads section with count", @@ -4229,7 +4204,6 @@ "@audioAnalysisSamples": { "description": "Total samples metric label" }, - "extensionsSearchWith": "Search with {providerName}", "@extensionsSearchWith": { "description": "Extensions page - subtitle for built-in search provider option", @@ -4249,7 +4223,7 @@ }, "extensionsHomeFeedAuto": "Auto", "@extensionsHomeFeedAuto": { - "description": "Extensions page - home feed provider option: auto" + "description": "Label for auto-selected search provider" }, "extensionsHomeFeedAutoSubtitle": "Automatically select the best available", "@extensionsHomeFeedAutoSubtitle": { @@ -4268,7 +4242,6 @@ "@extensionsNoHomeFeedExtensions": { "description": "Extensions page - shown when no installed extension has home feed" }, - "sortAlphaAsc": "A-Z", "@sortAlphaAsc": { "description": "Sort option - alphabetical ascending" @@ -4294,7 +4267,6 @@ "@cancelDownloadKeep": { "description": "Dialog button - keep the active download (do not cancel)" }, - "metadataSaveFailedFfmpeg": "Failed to save metadata via FFmpeg", "@metadataSaveFailedFfmpeg": { "description": "Snackbar error when FFmpeg fails to write metadata" @@ -4303,7 +4275,6 @@ "@metadataSaveFailedStorage": { "description": "Snackbar error when writing metadata file back to storage fails" }, - "snackbarFolderPickerFailed": "Failed to open folder picker: {error}", "@snackbarFolderPickerFailed": { "description": "Snackbar shown when folder picker fails to open", @@ -4313,7 +4284,6 @@ } } }, - "errorLoadAlbum": "Failed to load album", "@errorLoadAlbum": { "description": "Error state shown when album fails to load" @@ -4326,7 +4296,6 @@ "@errorLoadArtist": { "description": "Error state shown when artist fails to load" }, - "notifChannelDownloadName": "Download Progress", "@notifChannelDownloadName": { "description": "Android notification channel name for download progress" @@ -4540,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } -} +} \ No newline at end of file diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 248b1478..a17f7299 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1728,5 +1728,2858 @@ "type": "int" } } + }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, + "historySearchHint": "Search history...", + "@historySearchHint": { + "description": "Search bar placeholder in history" + }, + "downloadSingleFilenameFormat": "Single Filename Format", + "@downloadSingleFilenameFormat": { + "description": "Setting for output filename pattern for singles/EPs" + }, + "downloadSingleFilenameFormatDescription": "Filename pattern for singles and EPs. Uses the same tags as the album format.", + "@downloadSingleFilenameFormatDescription": { + "description": "Subtitle description for single filename format setting" + }, + "optionsDefaultSearchTab": "Default Search Tab", + "@optionsDefaultSearchTab": { + "description": "Title for the preferred default search tab setting" + }, + "optionsDefaultSearchTabSubtitle": "Choose which tab opens first for new search results.", + "@optionsDefaultSearchTabSubtitle": { + "description": "Subtitle for the preferred default search tab setting" + }, + "optionsReplayGain": "ReplayGain", + "@optionsReplayGain": { + "description": "Title for ReplayGain setting toggle" + }, + "optionsReplayGainSubtitleOn": "Scan loudness and embed ReplayGain tags (EBU R128)", + "@optionsReplayGainSubtitleOn": { + "description": "Subtitle when ReplayGain is enabled" + }, + "optionsReplayGainSubtitleOff": "Disabled: no loudness normalization tags", + "@optionsReplayGainSubtitleOff": { + "description": "Subtitle when ReplayGain is disabled" + }, + "optionsArtistTagMode": "Artist Tag Mode", + "@optionsArtistTagMode": { + "description": "Setting title for how artist metadata is written into files" + }, + "optionsArtistTagModeDescription": "Choose how multiple artists are written into embedded tags.", + "@optionsArtistTagModeDescription": { + "description": "Bottom-sheet description for artist tag mode setting" + }, + "optionsArtistTagModeJoined": "Single joined value", + "@optionsArtistTagModeJoined": { + "description": "Artist tag mode option that joins multiple artists into one value" + }, + "optionsArtistTagModeJoinedSubtitle": "Write one ARTIST value like \"Artist A, Artist B\" for maximum player compatibility.", + "@optionsArtistTagModeJoinedSubtitle": { + "description": "Subtitle for joined artist tag mode" + }, + "optionsArtistTagModeSplitVorbis": "Split tags for FLAC/Opus", + "@optionsArtistTagModeSplitVorbis": { + "description": "Artist tag mode option that writes repeated ARTIST tags for Vorbis formats" + }, + "optionsArtistTagModeSplitVorbisSubtitle": "Write one artist tag per artist for FLAC and Opus; MP3 and M4A stay joined.", + "@optionsArtistTagModeSplitVorbisSubtitle": { + "description": "Subtitle for split Vorbis artist tag mode" + }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, + "aboutTranslators": "Translators", + "@aboutTranslators": { + "description": "Section for translators" + }, + "aboutTelegramChannel": "Telegram Channel", + "@aboutTelegramChannel": { + "description": "Link to Telegram channel" + }, + "aboutTelegramChannelSubtitle": "Announcements and updates", + "@aboutTelegramChannelSubtitle": { + "description": "Subtitle for Telegram channel" + }, + "aboutTelegramChat": "Telegram Community", + "@aboutTelegramChat": { + "description": "Link to Telegram chat group" + }, + "aboutTelegramChatSubtitle": "Chat with other users", + "@aboutTelegramChatSubtitle": { + "description": "Subtitle for Telegram chat" + }, + "aboutSocial": "Social", + "@aboutSocial": { + "description": "Section for social links" + }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, + "artistPopular": "Popular", + "@artistPopular": { + "description": "Section header for popular/top tracks" + }, + "artistMonthlyListeners": "{count} monthly listeners", + "@artistMonthlyListeners": { + "description": "Monthly listener count display", + "placeholders": { + "count": { + "type": "String", + "description": "Formatted listener count" + } + } + }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, + "dialogDownload": "Download", + "@dialogDownload": { + "description": "Confirm button in Download All dialog" + }, + "csvImportTracks": "{count} tracks from CSV", + "@csvImportTracks": { + "description": "Label shown in quality picker for CSV import", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "errorUrlNotRecognized": "Link not recognized", + "@errorUrlNotRecognized": { + "description": "Error title - URL not handled by any extension or service" + }, + "errorUrlNotRecognizedMessage": "This link is not supported. Make sure the URL is correct and a compatible extension is installed.", + "@errorUrlNotRecognizedMessage": { + "description": "Error message - URL not recognized explanation" + }, + "errorUrlFetchFailed": "Failed to load content from this link. Please try again.", + "@errorUrlFetchFailed": { + "description": "Error message - generic URL fetch failure" + }, + "searchSortTitle": "Sort Results", + "@searchSortTitle": { + "description": "Bottom sheet title for search sort options" + }, + "searchSortDefault": "Default", + "@searchSortDefault": { + "description": "Sort option - default API order" + }, + "searchSortTitleAZ": "Title (A-Z)", + "@searchSortTitleAZ": { + "description": "Sort option - title ascending" + }, + "searchSortTitleZA": "Title (Z-A)", + "@searchSortTitleZA": { + "description": "Sort option - title descending" + }, + "searchSortArtistAZ": "Artist (A-Z)", + "@searchSortArtistAZ": { + "description": "Sort option - artist ascending" + }, + "searchSortArtistZA": "Artist (Z-A)", + "@searchSortArtistZA": { + "description": "Sort option - artist descending" + }, + "searchSortDurationShort": "Duration (Shortest)", + "@searchSortDurationShort": { + "description": "Sort option - shortest duration first" + }, + "searchSortDurationLong": "Duration (Longest)", + "@searchSortDurationLong": { + "description": "Sort option - longest duration first" + }, + "searchSortDateOldest": "Release Date (Oldest)", + "@searchSortDateOldest": { + "description": "Sort option - oldest release first" + }, + "searchSortDateNewest": "Release Date (Newest)", + "@searchSortDateNewest": { + "description": "Sort option - newest release first" + }, + "filenameShowAdvancedTags": "Show advanced tags", + "@filenameShowAdvancedTags": { + "description": "Toggle label for showing advanced filename tags" + }, + "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns", + "@filenameShowAdvancedTagsDescription": { + "description": "Description for advanced filename tag toggle" + }, + "folderOrganizationByPlaylist": "By Playlist", + "@folderOrganizationByPlaylist": { + "description": "Folder option - playlist folders" + }, + "folderOrganizationByPlaylistSubtitle": "Separate folder for each playlist", + "@folderOrganizationByPlaylistSubtitle": { + "description": "Subtitle for playlist folder option" + }, + "providerPriorityFallbackExtensionsTitle": "Extension Fallback", + "@providerPriorityFallbackExtensionsTitle": { + "description": "Section title for choosing which download extensions can be used as fallback providers" + }, + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "@providerPriorityFallbackExtensionsDescription": { + "description": "Section description for extension fallback selection" + }, + "providerPriorityFallbackExtensionsHint": "Only enabled extensions with download-provider capability are listed here.", + "@providerPriorityFallbackExtensionsHint": { + "description": "Hint below the extension fallback selection list" + }, + "sectionLyrics": "Lyrics", + "@sectionLyrics": { + "description": "Settings section header" + }, + "lyricsMode": "Lyrics Mode", + "@lyricsMode": { + "description": "Setting - how to save lyrics" + }, + "lyricsModeDescription": "Choose how lyrics are saved with your downloads", + "@lyricsModeDescription": { + "description": "Lyrics mode picker description" + }, + "lyricsModeEmbed": "Embed in file", + "@lyricsModeEmbed": { + "description": "Lyrics mode option - embed in audio file" + }, + "lyricsModeEmbedSubtitle": "Lyrics stored inside FLAC metadata", + "@lyricsModeEmbedSubtitle": { + "description": "Subtitle for embed option" + }, + "lyricsModeExternal": "External .lrc file", + "@lyricsModeExternal": { + "description": "Lyrics mode option - separate LRC file" + }, + "lyricsModeExternalSubtitle": "Separate .lrc file for players like Samsung Music", + "@lyricsModeExternalSubtitle": { + "description": "Subtitle for external option" + }, + "lyricsModeBoth": "Both", + "@lyricsModeBoth": { + "description": "Lyrics mode option - embed and external" + }, + "lyricsModeBothSubtitle": "Embed and save .lrc file", + "@lyricsModeBothSubtitle": { + "description": "Subtitle for both option" + }, + "trackGenre": "Genre", + "@trackGenre": { + "description": "Metadata label - music genre" + }, + "trackLabel": "Label", + "@trackLabel": { + "description": "Metadata label - record label" + }, + "trackCopyright": "Copyright", + "@trackCopyright": { + "description": "Metadata label - copyright information" + }, + "trackLyricsNotInFile": "No lyrics found in this file", + "@trackLyricsNotInFile": { + "description": "Message when no embedded lyrics in audio file" + }, + "trackFetchOnlineLyrics": "Fetch from Online", + "@trackFetchOnlineLyrics": { + "description": "Action - fetch lyrics from online providers" + }, + "trackEmbedLyrics": "Embed Lyrics", + "@trackEmbedLyrics": { + "description": "Action - embed lyrics into audio file" + }, + "trackLyricsEmbedded": "Lyrics embedded successfully", + "@trackLyricsEmbedded": { + "description": "Snackbar - lyrics saved to file" + }, + "trackInstrumental": "Instrumental track", + "@trackInstrumental": { + "description": "Message when track is instrumental (no lyrics)" + }, + "storeAddRepoTitle": "Add Extension Repository", + "@storeAddRepoTitle": { + "description": "Store setup screen - heading when no repo is configured" + }, + "storeAddRepoDescription": "Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.", + "@storeAddRepoDescription": { + "description": "Store setup screen - explanatory text" + }, + "storeRepoUrlLabel": "Repository URL", + "@storeRepoUrlLabel": { + "description": "Label for the repository URL input field" + }, + "storeRepoUrlHint": "https://github.com/user/repo", + "@storeRepoUrlHint": { + "description": "Hint/placeholder for the repository URL input field" + }, + "storeRepoUrlHelper": "e.g. https://github.com/user/extensions-repo", + "@storeRepoUrlHelper": { + "description": "Helper text below the repository URL input field" + }, + "storeAddRepoButton": "Add Repository", + "@storeAddRepoButton": { + "description": "Button to submit a new repository URL" + }, + "storeChangeRepoTooltip": "Change repository", + "@storeChangeRepoTooltip": { + "description": "Tooltip for the change-repository icon button in the app bar" + }, + "storeRepoDialogTitle": "Extension Repository", + "@storeRepoDialogTitle": { + "description": "Title of the change/remove repository dialog" + }, + "storeRepoDialogCurrent": "Current repository:", + "@storeRepoDialogCurrent": { + "description": "Label shown above the current repository URL in the dialog" + }, + "storeNewRepoUrlLabel": "New Repository URL", + "@storeNewRepoUrlLabel": { + "description": "Label for the new repository URL field inside the dialog" + }, + "storeLoadError": "Failed to load repository", + "@storeLoadError": { + "description": "Error heading when the store cannot be loaded" + }, + "storeEmptyNoExtensions": "No extensions available", + "@storeEmptyNoExtensions": { + "description": "Message when store has no extensions" + }, + "storeEmptyNoResults": "No extensions found", + "@storeEmptyNoResults": { + "description": "Message when search/filter returns no results" + }, + "extensionsFallbackTitle": "Fallback Extensions", + "@extensionsFallbackTitle": { + "description": "Setting and page title for choosing which download extensions can be used during fallback" + }, + "extensionsFallbackSubtitle": "Choose which installed download extensions can be used as fallback", + "@extensionsFallbackSubtitle": { + "description": "Subtitle for download fallback extensions menu" + }, + "downloadLossy320": "Lossy 320kbps", + "@downloadLossy320": { + "description": "Quality option label for Tidal lossy 320kbps" + }, + "downloadLossyFormat": "Lossy Format", + "@downloadLossyFormat": { + "description": "Setting title to pick output format for Tidal lossy downloads" + }, + "downloadLossy320Format": "Lossy 320kbps Format", + "@downloadLossy320Format": { + "description": "Title of the Tidal lossy format picker bottom sheet" + }, + "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", + "@downloadLossy320FormatDesc": { + "description": "Description in the Tidal lossy format picker" + }, + "downloadLossyMp3": "MP3 320kbps", + "@downloadLossyMp3": { + "description": "Tidal lossy format option - MP3 320kbps" + }, + "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", + "@downloadLossyMp3Subtitle": { + "description": "Subtitle for MP3 320kbps Tidal lossy option" + }, + "downloadLossyOpus256": "Opus 256kbps", + "@downloadLossyOpus256": { + "description": "Tidal lossy format option - Opus 256kbps" + }, + "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", + "@downloadLossyOpus256Subtitle": { + "description": "Subtitle for Opus 256kbps Tidal lossy option" + }, + "downloadLossyOpus128": "Opus 128kbps", + "@downloadLossyOpus128": { + "description": "Tidal lossy format option - Opus 128kbps" + }, + "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", + "@downloadLossyOpus128Subtitle": { + "description": "Subtitle for Opus 128kbps Tidal lossy option" + }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, + "albumFolderArtistAlbumSingles": "Artist / Album + Singles", + "@albumFolderArtistAlbumSingles": { + "description": "Album folder option with singles inside artist" + }, + "albumFolderArtistAlbumSinglesSubtitle": "Artist/Album/ and Artist/Singles/", + "@albumFolderArtistAlbumSinglesSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistAlbumFlat": "Artist / Album (Singles flat)", + "@albumFolderArtistAlbumFlat": { + "description": "Album folder option with singles directly in artist folder" + }, + "albumFolderArtistAlbumFlatSubtitle": "Artist/Album/ and Artist/song.flac", + "@albumFolderArtistAlbumFlatSubtitle": { + "description": "Folder structure example for flat singles" + }, + "downloadedAlbumDiscHeader": "Disc {discNumber}", + "@downloadedAlbumDiscHeader": { + "description": "Header for disc separator in multi-disc albums", + "placeholders": { + "discNumber": { + "type": "int", + "example": "1" + } + } + }, + "recentTypeArtist": "Artist", + "@recentTypeArtist": { + "description": "Recent access item type - artist" + }, + "recentTypeAlbum": "Album", + "@recentTypeAlbum": { + "description": "Recent access item type - album" + }, + "recentTypeSong": "Song", + "@recentTypeSong": { + "description": "Recent access item type - song/track" + }, + "recentTypePlaylist": "Playlist", + "@recentTypePlaylist": { + "description": "Recent access item type - playlist" + }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, + "recentPlaylistInfo": "Playlist: {name}", + "@recentPlaylistInfo": { + "description": "Snackbar message when tapping playlist in recent access", + "placeholders": { + "name": { + "type": "String", + "description": "Playlist name" + } + } + }, + "discographyDownload": "Download Discography", + "@discographyDownload": { + "description": "Button - download artist discography" + }, + "discographyDownloadAll": "Download All", + "@discographyDownloadAll": { + "description": "Option - download entire discography" + }, + "discographyDownloadAllSubtitle": "{count} tracks from {albumCount} releases", + "@discographyDownloadAllSubtitle": { + "description": "Subtitle showing total tracks and albums", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographyAlbumsOnly": "Albums Only", + "@discographyAlbumsOnly": { + "description": "Option - download only albums" + }, + "discographyAlbumsOnlySubtitle": "{count} tracks from {albumCount} albums", + "@discographyAlbumsOnlySubtitle": { + "description": "Subtitle showing album tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySinglesOnly": "Singles & EPs Only", + "@discographySinglesOnly": { + "description": "Option - download only singles" + }, + "discographySinglesOnlySubtitle": "{count} tracks from {albumCount} singles", + "@discographySinglesOnlySubtitle": { + "description": "Subtitle showing singles tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySelectAlbums": "Select Albums...", + "@discographySelectAlbums": { + "description": "Option - manually select albums to download" + }, + "discographySelectAlbumsSubtitle": "Choose specific albums or singles", + "@discographySelectAlbumsSubtitle": { + "description": "Subtitle for select albums option" + }, + "discographyFetchingTracks": "Fetching tracks...", + "@discographyFetchingTracks": { + "description": "Progress - fetching album tracks" + }, + "discographyFetchingAlbum": "Fetching {current} of {total}...", + "@discographyFetchingAlbum": { + "description": "Progress - fetching specific album", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "discographySelectedCount": "{count} selected", + "@discographySelectedCount": { + "description": "Selection count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographyDownloadSelected": "Download Selected", + "@discographyDownloadSelected": { + "description": "Button - download selected albums" + }, + "discographyAddedToQueue": "Added {count} tracks to queue", + "@discographyAddedToQueue": { + "description": "Snackbar - tracks added from discography", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographySkippedDownloaded": "{added} added, {skipped} already downloaded", + "@discographySkippedDownloaded": { + "description": "Snackbar - with skipped tracks count", + "placeholders": { + "added": { + "type": "int" + }, + "skipped": { + "type": "int" + } + } + }, + "discographyNoAlbums": "No albums available", + "@discographyNoAlbums": { + "description": "Error - no albums found for artist" + }, + "discographyFailedToFetch": "Failed to fetch some albums", + "@discographyFailedToFetch": { + "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryAutoScan": "Auto Scan", + "@libraryAutoScan": { + "description": "Setting for automatic library scanning" + }, + "libraryAutoScanSubtitle": "Automatically scan your library for new files", + "@libraryAutoScanSubtitle": { + "description": "Subtitle for auto scan setting" + }, + "libraryAutoScanOff": "Off", + "@libraryAutoScanOff": { + "description": "Auto scan disabled" + }, + "libraryAutoScanOnOpen": "Every app open", + "@libraryAutoScanOnOpen": { + "description": "Auto scan when app opens" + }, + "libraryAutoScanDaily": "Daily", + "@libraryAutoScanDaily": { + "description": "Auto scan once per day" + }, + "libraryAutoScanWeekly": "Weekly", + "@libraryAutoScanWeekly": { + "description": "Auto scan once per week" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}", + "@libraryTracksUnit": { + "description": "Unit label for tracks count (without the number itself)", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryFilesUnit": "{count, plural, =1{file} other{files}}", + "@libraryFilesUnit": { + "description": "Unit label for files count during library scanning", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanFinalizing": "Finalizing library...", + "@libraryScanFinalizing": { + "description": "Status shown after file scanning finishes but library persistence is still running" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterMetadata": "Metadata", + "@libraryFilterMetadata": { + "description": "Filter section - metadata completeness" + }, + "libraryFilterMetadataComplete": "Complete metadata", + "@libraryFilterMetadataComplete": { + "description": "Filter option - items with complete metadata" + }, + "libraryFilterMetadataMissingAny": "Missing any metadata", + "@libraryFilterMetadataMissingAny": { + "description": "Filter option - items missing any tracked metadata field" + }, + "libraryFilterMetadataMissingYear": "Missing year", + "@libraryFilterMetadataMissingYear": { + "description": "Filter option - items missing release year/date" + }, + "libraryFilterMetadataMissingGenre": "Missing genre", + "@libraryFilterMetadataMissingGenre": { + "description": "Filter option - items missing genre" + }, + "libraryFilterMetadataMissingAlbumArtist": "Missing album artist", + "@libraryFilterMetadataMissingAlbumArtist": { + "description": "Filter option - items missing album artist" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterSortAlbumAsc": "Album (A-Z)", + "@libraryFilterSortAlbumAsc": { + "description": "Sort option - album ascending" + }, + "libraryFilterSortAlbumDesc": "Album (Z-A)", + "@libraryFilterSortAlbumDesc": { + "description": "Sort option - album descending" + }, + "libraryFilterSortGenreAsc": "Genre (A-Z)", + "@libraryFilterSortGenreAsc": { + "description": "Sort option - genre ascending" + }, + "libraryFilterSortGenreDesc": "Genre (Z-A)", + "@libraryFilterSortGenreDesc": { + "description": "Sort option - genre descending" + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Deezer", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Repo tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackReEnrichFieldsTitle": "Fields to update", + "@trackReEnrichFieldsTitle": { + "description": "Section title for field selection in re-enrich dialog" + }, + "trackReEnrichFieldCover": "Cover Art", + "@trackReEnrichFieldCover": { + "description": "Checkbox label for cover art field in re-enrich" + }, + "trackReEnrichFieldLyrics": "Lyrics", + "@trackReEnrichFieldLyrics": { + "description": "Checkbox label for lyrics field in re-enrich" + }, + "trackReEnrichFieldBasicTags": "Album, Album Artist", + "@trackReEnrichFieldBasicTags": { + "description": "Checkbox label for basic tags in re-enrich (title/artist are never overwritten)" + }, + "trackReEnrichFieldTrackInfo": "Track & Disc Number", + "@trackReEnrichFieldTrackInfo": { + "description": "Checkbox label for track info in re-enrich" + }, + "trackReEnrichFieldReleaseInfo": "Date & ISRC", + "@trackReEnrichFieldReleaseInfo": { + "description": "Checkbox label for release info in re-enrich" + }, + "trackReEnrichFieldExtra": "Genre, Label, Copyright", + "@trackReEnrichFieldExtra": { + "description": "Checkbox label for extra metadata in re-enrich" + }, + "trackReEnrichSelectAll": "Select All", + "@trackReEnrichSelectAll": { + "description": "Select all fields checkbox in re-enrich" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "queueFlacAction": "Queue FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Finding FLAC matches... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "No reliable online matches found for the selection", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Added {addedCount} tracks to queue, skipped {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3, Opus, ALAC, or FLAC", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConfirmMessageLossless": "Convert from {sourceFormat} to {targetFormat}? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless-to-lossless conversion", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + } + } + }, + "trackConvertLosslessHint": "Lossless conversion — no quality loss", + "@trackConvertLosslessHint": { + "description": "Hint shown when converting between lossless formats" + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" + }, + "cueSplitTitle": "Split CUE Sheet", + "@cueSplitTitle": { + "description": "Title for CUE split bottom sheet" + }, + "cueSplitSubtitle": "Split CUE+FLAC into individual tracks", + "@cueSplitSubtitle": { + "description": "Subtitle for CUE split menu item" + }, + "cueSplitAlbum": "Album: {album}", + "@cueSplitAlbum": { + "description": "Album name in CUE split sheet", + "placeholders": { + "album": { + "type": "String" + } + } + }, + "cueSplitArtist": "Artist: {artist}", + "@cueSplitArtist": { + "description": "Artist name in CUE split sheet", + "placeholders": { + "artist": { + "type": "String" + } + } + }, + "cueSplitTrackCount": "{count} tracks", + "@cueSplitTrackCount": { + "description": "Number of tracks in CUE sheet", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitConfirmTitle": "Split CUE Album", + "@cueSplitConfirmTitle": { + "description": "CUE split confirmation dialog title" + }, + "cueSplitConfirmMessage": "Split \"{album}\" into {count} individual FLAC files?\n\nFiles will be saved to the same directory.", + "@cueSplitConfirmMessage": { + "description": "CUE split confirmation dialog message", + "placeholders": { + "album": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})", + "@cueSplitSplitting": { + "description": "Snackbar while splitting CUE", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "cueSplitSuccess": "Split into {count} tracks successfully", + "@cueSplitSuccess": { + "description": "Snackbar after successful CUE split", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitFailed": "CUE split failed", + "@cueSplitFailed": { + "description": "Snackbar when CUE split fails" + }, + "cueSplitNoAudioFile": "Audio file not found for this CUE sheet", + "@cueSplitNoAudioFile": { + "description": "Error when CUE audio file is missing" + }, + "cueSplitButton": "Split into Tracks", + "@cueSplitButton": { + "description": "Button text to start CUE splitting" + }, + "actionCreate": "Create", + "@actionCreate": { + "description": "Generic action button - create" + }, + "collectionFoldersTitle": "My folders", + "@collectionFoldersTitle": { + "description": "Library section title for custom folders" + }, + "collectionWishlist": "Wishlist", + "@collectionWishlist": { + "description": "Custom folder for saved tracks to download later" + }, + "collectionLoved": "Loved", + "@collectionLoved": { + "description": "Custom folder for favorite tracks" + }, + "collectionPlaylists": "Playlists", + "@collectionPlaylists": { + "description": "Custom user playlists folder" + }, + "collectionPlaylist": "Playlist", + "@collectionPlaylist": { + "description": "Single playlist label" + }, + "collectionAddToPlaylist": "Add to playlist", + "@collectionAddToPlaylist": { + "description": "Action to add a track to user playlist" + }, + "collectionCreatePlaylist": "Create playlist", + "@collectionCreatePlaylist": { + "description": "Action to create a new playlist" + }, + "collectionNoPlaylistsYet": "No playlists yet", + "@collectionNoPlaylistsYet": { + "description": "Empty state title when user has no playlists" + }, + "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks", + "@collectionNoPlaylistsSubtitle": { + "description": "Empty state subtitle when user has no playlists" + }, + "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@collectionPlaylistTracks": { + "description": "Track count label for custom playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "collectionAddedToPlaylist": "Added to \"{playlistName}\"", + "@collectionAddedToPlaylist": { + "description": "Snackbar after adding track to playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"", + "@collectionAlreadyInPlaylist": { + "description": "Snackbar when track already exists in playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionPlaylistCreated": "Playlist created", + "@collectionPlaylistCreated": { + "description": "Snackbar after creating playlist" + }, + "collectionPlaylistNameHint": "Playlist name", + "@collectionPlaylistNameHint": { + "description": "Hint text for playlist name input" + }, + "collectionPlaylistNameRequired": "Playlist name is required", + "@collectionPlaylistNameRequired": { + "description": "Validation error for empty playlist name" + }, + "collectionRenamePlaylist": "Rename playlist", + "@collectionRenamePlaylist": { + "description": "Action to rename playlist" + }, + "collectionDeletePlaylist": "Delete playlist", + "@collectionDeletePlaylist": { + "description": "Action to delete playlist" + }, + "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?", + "@collectionDeletePlaylistMessage": { + "description": "Confirmation message for deleting playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionPlaylistDeleted": "Playlist deleted", + "@collectionPlaylistDeleted": { + "description": "Snackbar after deleting playlist" + }, + "collectionPlaylistRenamed": "Playlist renamed", + "@collectionPlaylistRenamed": { + "description": "Snackbar after renaming playlist" + }, + "collectionWishlistEmptyTitle": "Wishlist is empty", + "@collectionWishlistEmptyTitle": { + "description": "Wishlist empty state title" + }, + "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later", + "@collectionWishlistEmptySubtitle": { + "description": "Wishlist empty state subtitle" + }, + "collectionLovedEmptyTitle": "Loved folder is empty", + "@collectionLovedEmptyTitle": { + "description": "Loved empty state title" + }, + "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites", + "@collectionLovedEmptySubtitle": { + "description": "Loved empty state subtitle" + }, + "collectionPlaylistEmptyTitle": "Playlist is empty", + "@collectionPlaylistEmptyTitle": { + "description": "Playlist empty state title" + }, + "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here", + "@collectionPlaylistEmptySubtitle": { + "description": "Playlist empty state subtitle" + }, + "collectionRemoveFromPlaylist": "Remove from playlist", + "@collectionRemoveFromPlaylist": { + "description": "Tooltip for removing track from playlist" + }, + "collectionRemoveFromFolder": "Remove from folder", + "@collectionRemoveFromFolder": { + "description": "Tooltip for removing track from wishlist/loved folder" + }, + "collectionRemoved": "\"{trackName}\" removed", + "@collectionRemoved": { + "description": "Snackbar after removing a track from a collection", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionAddedToLoved": "\"{trackName}\" added to Loved", + "@collectionAddedToLoved": { + "description": "Snackbar after adding track to loved folder", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved", + "@collectionRemovedFromLoved": { + "description": "Snackbar after removing track from loved folder", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist", + "@collectionAddedToWishlist": { + "description": "Snackbar after adding track to wishlist", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist", + "@collectionRemovedFromWishlist": { + "description": "Snackbar after removing track from wishlist", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "trackOptionAddToLoved": "Add to Loved", + "@trackOptionAddToLoved": { + "description": "Bottom sheet action label - add track to loved folder" + }, + "trackOptionRemoveFromLoved": "Remove from Loved", + "@trackOptionRemoveFromLoved": { + "description": "Bottom sheet action label - remove track from loved folder" + }, + "trackOptionAddToWishlist": "Add to Wishlist", + "@trackOptionAddToWishlist": { + "description": "Bottom sheet action label - add track to wishlist" + }, + "trackOptionRemoveFromWishlist": "Remove from Wishlist", + "@trackOptionRemoveFromWishlist": { + "description": "Bottom sheet action label - remove track from wishlist" + }, + "collectionPlaylistChangeCover": "Change cover image", + "@collectionPlaylistChangeCover": { + "description": "Bottom sheet action to pick a custom cover image for a playlist" + }, + "collectionPlaylistRemoveCover": "Remove cover image", + "@collectionPlaylistRemoveCover": { + "description": "Bottom sheet action to remove custom cover image from a playlist" + }, + "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}", + "@selectionShareCount": { + "description": "Share button text with count in selection mode", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionShareNoFiles": "No shareable files found", + "@selectionShareNoFiles": { + "description": "Snackbar when no selected files exist on disk" + }, + "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}", + "@selectionConvertCount": { + "description": "Convert button text with count in selection mode", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionConvertNoConvertible": "No convertible tracks selected", + "@selectionConvertNoConvertible": { + "description": "Snackbar when no selected tracks support conversion" + }, + "selectionBatchConvertConfirmTitle": "Batch Convert", + "@selectionBatchConvertConfirmTitle": { + "description": "Confirmation dialog title for batch conversion" + }, + "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessage": { + "description": "Confirmation dialog message for batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "selectionBatchConvertConfirmMessageLossless": "Convert {count} {count, plural, =1{track} other{tracks}} to {format}? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, + "selectionBatchConvertProgress": "Converting {current} of {total}...", + "@selectionBatchConvertProgress": { + "description": "Snackbar during batch conversion progress", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}", + "@selectionBatchConvertSuccess": { + "description": "Snackbar after batch conversion completes", + "placeholders": { + "success": { + "type": "int" + }, + "total": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Folder named after Album Artist tag", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when album artist is used for folder names" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Folder named after Track Artist tag", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when track artist is used for folder names" + }, + "lyricsProvidersTitle": "Lyrics Provider Priority", + "@lyricsProvidersTitle": { + "description": "Settings item title for lyrics provider order" + }, + "lyricsProvidersDescription": "Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.", + "@lyricsProvidersDescription": { + "description": "Description on the lyrics provider priority page" + }, + "lyricsProvidersInfoText": "Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.", + "@lyricsProvidersInfoText": { + "description": "Info tip on lyrics provider priority page" + }, + "lyricsProvidersEnabledSection": "Enabled ({count})", + "@lyricsProvidersEnabledSection": { + "description": "Section header for enabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersDisabledSection": "Disabled ({count})", + "@lyricsProvidersDisabledSection": { + "description": "Section header for disabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersAtLeastOne": "At least one provider must remain enabled", + "@lyricsProvidersAtLeastOne": { + "description": "Snackbar when user tries to disable the last enabled provider" + }, + "lyricsProvidersSaved": "Lyrics provider priority saved", + "@lyricsProvidersSaved": { + "description": "Snackbar after saving lyrics provider priority" + }, + "lyricsProvidersDiscardContent": "You have unsaved changes that will be lost.", + "@lyricsProvidersDiscardContent": { + "description": "Body text of the discard-changes dialog on lyrics provider page" + }, + "lyricsProviderLrclibDesc": "Open-source synced lyrics database", + "@lyricsProviderLrclibDesc": { + "description": "Description for LRCLIB provider" + }, + "lyricsProviderNeteaseDesc": "NetEase Cloud Music (good for Asian songs)", + "@lyricsProviderNeteaseDesc": { + "description": "Description for Netease provider" + }, + "lyricsProviderMusixmatchDesc": "Largest lyrics database (multi-language)", + "@lyricsProviderMusixmatchDesc": { + "description": "Description for Musixmatch provider" + }, + "lyricsProviderAppleMusicDesc": "Word-by-word synced lyrics (via proxy)", + "@lyricsProviderAppleMusicDesc": { + "description": "Description for Apple Music provider" + }, + "lyricsProviderQqMusicDesc": "QQ Music (good for Chinese songs, via proxy)", + "@lyricsProviderQqMusicDesc": { + "description": "Description for QQ Music provider" + }, + "lyricsProviderExtensionDesc": "Extension provider", + "@lyricsProviderExtensionDesc": { + "description": "Generic description for extension-based lyrics providers" + }, + "safMigrationTitle": "Storage Update Required", + "@safMigrationTitle": { + "description": "Title of SAF migration dialog" + }, + "safMigrationMessage1": "SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.", + "@safMigrationMessage1": { + "description": "First paragraph of SAF migration dialog" + }, + "safMigrationMessage2": "Please select your download folder again to switch to the new storage system.", + "@safMigrationMessage2": { + "description": "Second paragraph of SAF migration dialog" + }, + "safMigrationSuccess": "Download folder updated to SAF mode", + "@safMigrationSuccess": { + "description": "Snackbar after successfully migrating to SAF" + }, + "settingsDonate": "Support Development", + "@settingsDonate": { + "description": "Settings menu item - donate page" + }, + "settingsDonateSubtitle": "Buy the developer a coffee", + "@settingsDonateSubtitle": { + "description": "Subtitle for donate menu item" + }, + "tooltipLoveAll": "Love All", + "@tooltipLoveAll": { + "description": "Tooltip for the Love All button on album/playlist screens" + }, + "tooltipAddToPlaylist": "Add to Playlist", + "@tooltipAddToPlaylist": { + "description": "Tooltip for the Add to Playlist button" + }, + "snackbarRemovedTracksFromLoved": "Removed {count} tracks from Loved", + "@snackbarRemovedTracksFromLoved": { + "description": "Snackbar after removing multiple tracks from Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedTracksToLoved": "Added {count} tracks to Loved", + "@snackbarAddedTracksToLoved": { + "description": "Snackbar after adding multiple tracks to Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogDownloadAllTitle": "Download All", + "@dialogDownloadAllTitle": { + "description": "Dialog title for bulk download confirmation" + }, + "dialogDownloadAllMessage": "Download {count} tracks?", + "@dialogDownloadAllMessage": { + "description": "Body of the Download All confirmation dialog", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "homeSkipAlreadyDownloaded": "Skip already downloaded songs", + "@homeSkipAlreadyDownloaded": { + "description": "Checkbox label in import dialog to skip already-downloaded songs" + }, + "homeGoToAlbum": "Go to Album", + "@homeGoToAlbum": { + "description": "Context menu item to navigate to the album page" + }, + "homeAlbumInfoUnavailable": "Album info not available", + "@homeAlbumInfoUnavailable": { + "description": "Snackbar when album info cannot be loaded" + }, + "snackbarLoadingCueSheet": "Loading CUE sheet...", + "@snackbarLoadingCueSheet": { + "description": "Snackbar while loading a CUE sheet file" + }, + "snackbarMetadataSaved": "Metadata saved successfully", + "@snackbarMetadataSaved": { + "description": "Snackbar after successfully saving track metadata" + }, + "snackbarFailedToEmbedLyrics": "Failed to embed lyrics", + "@snackbarFailedToEmbedLyrics": { + "description": "Snackbar when lyrics embedding fails" + }, + "snackbarFailedToWriteStorage": "Failed to write back to storage", + "@snackbarFailedToWriteStorage": { + "description": "Snackbar when writing metadata back to file fails" + }, + "snackbarError": "Error: {error}", + "@snackbarError": { + "description": "Generic error snackbar with error detail", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarNoActionDefined": "No action defined for this button", + "@snackbarNoActionDefined": { + "description": "Snackbar when an extension button has no action configured" + }, + "noTracksFoundForAlbum": "No tracks found for this album", + "@noTracksFoundForAlbum": { + "description": "Empty state message when an album has no tracks" + }, + "downloadLocationSubtitle": "Choose where to save your downloaded tracks", + "@downloadLocationSubtitle": { + "description": "Subtitle shown in the download location picker sheet" + }, + "storageModeAppFolder": "App Folder (Recommended)", + "@storageModeAppFolder": { + "description": "Storage mode option - app-managed folder" + }, + "storageModeAppFolderSubtitle": "Saves to Music/SpotiFLAC by default", + "@storageModeAppFolderSubtitle": { + "description": "Subtitle for app folder storage mode" + }, + "storageModeSaf": "Custom Folder (SAF)", + "@storageModeSaf": { + "description": "Storage mode option - Storage Access Framework" + }, + "storageModeSafSubtitle": "Pick any folder, including SD card", + "@storageModeSafSubtitle": { + "description": "Subtitle for SAF storage mode" + }, + "downloadFilenameDescription": "Use {artist}, {title}, {album}, {track}, {year}, {date}, {disc} as placeholders.", + "@downloadFilenameDescription": { + "description": "Description shown in filename format editor" + }, + "downloadFilenameInsertTag": "Tap to insert tag:", + "@downloadFilenameInsertTag": { + "description": "Label above filename tag chips" + }, + "downloadSeparateSinglesEnabled": "Singles and EPs saved in a separate folder", + "@downloadSeparateSinglesEnabled": { + "description": "Subtitle when separate singles folder is on" + }, + "downloadSeparateSinglesDisabled": "Singles and albums saved in the same folder", + "@downloadSeparateSinglesDisabled": { + "description": "Subtitle when separate singles folder is off" + }, + "downloadArtistNameFilters": "Artist Name Filters", + "@downloadArtistNameFilters": { + "description": "Setting title for artist folder filter options" + }, + "downloadCreatePlaylistSourceFolder": "Playlist Source Folder", + "@downloadCreatePlaylistSourceFolder": { + "description": "Setting to create a subfolder per playlist source" + }, + "downloadCreatePlaylistSourceFolderEnabled": "A subfolder is created for each playlist", + "@downloadCreatePlaylistSourceFolderEnabled": { + "description": "Subtitle when playlist folder is enabled" + }, + "downloadCreatePlaylistSourceFolderDisabled": "All tracks saved directly to download folder", + "@downloadCreatePlaylistSourceFolderDisabled": { + "description": "Subtitle when playlist folder is disabled" + }, + "downloadCreatePlaylistSourceFolderRedundant": "Handled by folder organization setting", + "@downloadCreatePlaylistSourceFolderRedundant": { + "description": "Subtitle when folder organization is already set to playlist" + }, + "downloadSongLinkRegion": "SongLink Region", + "@downloadSongLinkRegion": { + "description": "Setting for SongLink region used during fallback resolution" + }, + "downloadNetworkCompatibilityMode": "Network Compatibility Mode", + "@downloadNetworkCompatibilityMode": { + "description": "Setting for legacy TLS/network handling" + }, + "downloadNetworkCompatibilityModeEnabled": "Using legacy TLS settings for older networks", + "@downloadNetworkCompatibilityModeEnabled": { + "description": "Subtitle when network compatibility mode is on" + }, + "downloadNetworkCompatibilityModeDisabled": "Using standard network settings", + "@downloadNetworkCompatibilityModeDisabled": { + "description": "Subtitle when network compatibility mode is off" + }, + "downloadSelectServiceToEnable": "Select Tidal or Qobuz to enable this option", + "@downloadSelectServiceToEnable": { + "description": "Subtitle when quality picker is disabled due to extension service" + }, + "downloadSelectTidalQobuz": "Select Tidal or Qobuz to choose audio quality", + "@downloadSelectTidalQobuz": { + "description": "Info shown when a non-built-in service is selected" + }, + "downloadEmbedLyricsDisabled": "Enable metadata embedding first", + "@downloadEmbedLyricsDisabled": { + "description": "Subtitle when lyrics embedding is blocked by metadata toggle" + }, + "downloadNeteaseIncludeTranslation": "Netease: Include Translation", + "@downloadNeteaseIncludeTranslation": { + "description": "Setting to include translated lyrics from Netease" + }, + "downloadNeteaseIncludeTranslationEnabled": "Chinese translation lines included", + "@downloadNeteaseIncludeTranslationEnabled": { + "description": "Subtitle when Netease translation is on" + }, + "downloadNeteaseIncludeTranslationDisabled": "Original lyrics only", + "@downloadNeteaseIncludeTranslationDisabled": { + "description": "Subtitle when Netease translation is off" + }, + "downloadNeteaseIncludeRomanization": "Netease: Include Romanization", + "@downloadNeteaseIncludeRomanization": { + "description": "Setting to include romanized lyrics from Netease" + }, + "downloadNeteaseIncludeRomanizationEnabled": "Romanization lines included", + "@downloadNeteaseIncludeRomanizationEnabled": { + "description": "Subtitle when Netease romanization is on" + }, + "downloadNeteaseIncludeRomanizationDisabled": "No romanization", + "@downloadNeteaseIncludeRomanizationDisabled": { + "description": "Subtitle when Netease romanization is off" + }, + "downloadAppleQqMultiPerson": "Apple / QQ: Multi-Person Lyrics", + "@downloadAppleQqMultiPerson": { + "description": "Setting for word-by-word multi-person lyrics from Apple Music and QQ Music" + }, + "downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks", + "@downloadAppleQqMultiPersonEnabled": { + "description": "Subtitle when multi-person lyrics is on" + }, + "downloadAppleQqMultiPersonDisabled": "Standard lyrics without speaker labels", + "@downloadAppleQqMultiPersonDisabled": { + "description": "Subtitle when multi-person lyrics is off" + }, + "downloadMusixmatchLanguage": "Musixmatch Language", + "@downloadMusixmatchLanguage": { + "description": "Setting for Musixmatch lyrics translation language" + }, + "downloadMusixmatchLanguageAuto": "Auto (original language)", + "@downloadMusixmatchLanguageAuto": { + "description": "Subtitle when no language is set" + }, + "downloadFilterContributing": "Filter Contributing Artists", + "@downloadFilterContributing": { + "description": "Setting to strip contributing artists from Album Artist folder name" + }, + "downloadFilterContributingEnabled": "Contributing artists removed from Album Artist folder name", + "@downloadFilterContributingEnabled": { + "description": "Subtitle when contributing artist filter is on" + }, + "downloadFilterContributingDisabled": "Full Album Artist string used", + "@downloadFilterContributingDisabled": { + "description": "Subtitle when contributing artist filter is off" + }, + "downloadProvidersNoneEnabled": "No providers enabled", + "@downloadProvidersNoneEnabled": { + "description": "Shown when no lyrics providers are active" + }, + "downloadMusixmatchLanguageCode": "Language code", + "@downloadMusixmatchLanguageCode": { + "description": "Label for Musixmatch language input field" + }, + "downloadMusixmatchLanguageHint": "e.g. en, de, ja", + "@downloadMusixmatchLanguageHint": { + "description": "Placeholder for Musixmatch language input" + }, + "downloadMusixmatchLanguageDesc": "Enter a BCP-47 language code (e.g. en, de, ja) to request translated lyrics from Musixmatch.", + "@downloadMusixmatchLanguageDesc": { + "description": "Description in Musixmatch language picker" + }, + "downloadMusixmatchAuto": "Auto", + "@downloadMusixmatchAuto": { + "description": "Button to clear Musixmatch language (use auto)" + }, + "downloadNetworkAnySubtitle": "Use WiFi or mobile data", + "@downloadNetworkAnySubtitle": { + "description": "Subtitle for any-network option in picker" + }, + "downloadNetworkWifiOnlySubtitle": "Downloads pause when on mobile data", + "@downloadNetworkWifiOnlySubtitle": { + "description": "Subtitle for WiFi-only option in picker" + }, + "downloadSongLinkRegionDesc": "Region used when resolving track links via SongLink. Choose the country where your streaming services are available.", + "@downloadSongLinkRegionDesc": { + "description": "Description in SongLink region picker" + }, + "snackbarUnsupportedAudioFormat": "Unsupported audio format", + "@snackbarUnsupportedAudioFormat": { + "description": "Snackbar when the audio format is not supported for the requested operation" + }, + "cacheRefresh": "Refresh", + "@cacheRefresh": { + "description": "Tooltip for refresh button on cache management page" + }, + "dialogDownloadPlaylistsMessage": "Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?", + "@dialogDownloadPlaylistsMessage": { + "description": "Dialog message for bulk playlist download confirmation", + "placeholders": { + "trackCount": { + "type": "int" + }, + "playlistCount": { + "type": "int" + } + } + }, + "bulkDownloadPlaylistsButton": "Download {count} {count, plural, =1{playlist} other{playlists}}", + "@bulkDownloadPlaylistsButton": { + "description": "Button label for bulk downloading selected playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "bulkDownloadSelectPlaylists": "Select playlists to download", + "@bulkDownloadSelectPlaylists": { + "description": "Button label when no playlists are selected for download" + }, + "snackbarSelectedPlaylistsEmpty": "Selected playlists have no tracks", + "@snackbarSelectedPlaylistsEmpty": { + "description": "Snackbar when selected playlists contain no tracks" + }, + "playlistsCount": "{count, plural, =1{1 playlist} other{{count} playlists}}", + "@playlistsCount": { + "description": "Playlist count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFill": "Auto-fill from online", + "@editMetadataAutoFill": { + "description": "Section title for selective online metadata auto-fill in the edit metadata sheet" + }, + "editMetadataAutoFillDesc": "Select fields to fill automatically from online metadata", + "@editMetadataAutoFillDesc": { + "description": "Description for the auto-fill section" + }, + "editMetadataAutoFillFetch": "Fetch & Fill", + "@editMetadataAutoFillFetch": { + "description": "Button label to fetch online metadata and fill selected fields" + }, + "editMetadataAutoFillSearching": "Searching online...", + "@editMetadataAutoFillSearching": { + "description": "Snackbar shown while searching for online metadata" + }, + "editMetadataAutoFillNoResults": "No matching metadata found online", + "@editMetadataAutoFillNoResults": { + "description": "Snackbar when online metadata search returns no results" + }, + "editMetadataAutoFillDone": "Filled {count} {count, plural, =1{field} other{fields}} from online metadata", + "@editMetadataAutoFillDone": { + "description": "Snackbar confirming how many fields were auto-filled", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFillNoneSelected": "Select at least one field to auto-fill", + "@editMetadataAutoFillNoneSelected": { + "description": "Snackbar when user taps Fetch without selecting any fields" + }, + "editMetadataFieldTitle": "Title", + "@editMetadataFieldTitle": { + "description": "Chip label for title field in auto-fill selector" + }, + "editMetadataFieldArtist": "Artist", + "@editMetadataFieldArtist": { + "description": "Chip label for artist field in auto-fill selector" + }, + "editMetadataFieldAlbum": "Album", + "@editMetadataFieldAlbum": { + "description": "Chip label for album field in auto-fill selector" + }, + "editMetadataFieldAlbumArtist": "Album Artist", + "@editMetadataFieldAlbumArtist": { + "description": "Chip label for album artist field in auto-fill selector" + }, + "editMetadataFieldDate": "Date", + "@editMetadataFieldDate": { + "description": "Chip label for date field in auto-fill selector" + }, + "editMetadataFieldTrackNum": "Track #", + "@editMetadataFieldTrackNum": { + "description": "Chip label for track number field in auto-fill selector" + }, + "editMetadataFieldDiscNum": "Disc #", + "@editMetadataFieldDiscNum": { + "description": "Chip label for disc number field in auto-fill selector" + }, + "editMetadataFieldGenre": "Genre", + "@editMetadataFieldGenre": { + "description": "Chip label for genre field in auto-fill selector" + }, + "editMetadataFieldIsrc": "ISRC", + "@editMetadataFieldIsrc": { + "description": "Chip label for ISRC field in auto-fill selector" + }, + "editMetadataFieldLabel": "Label", + "@editMetadataFieldLabel": { + "description": "Chip label for label field in auto-fill selector" + }, + "editMetadataFieldCopyright": "Copyright", + "@editMetadataFieldCopyright": { + "description": "Chip label for copyright field in auto-fill selector" + }, + "editMetadataFieldCover": "Cover Art", + "@editMetadataFieldCover": { + "description": "Chip label for cover art field in auto-fill selector" + }, + "editMetadataSelectAll": "All", + "@editMetadataSelectAll": { + "description": "Button to select all fields for auto-fill" + }, + "editMetadataSelectEmpty": "Empty only", + "@editMetadataSelectEmpty": { + "description": "Button to select only fields that are currently empty" + }, + "queueDownloadingCount": "Downloading ({count})", + "@queueDownloadingCount": { + "description": "Header for active downloads section with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueDownloadedHeader": "Downloaded", + "@queueDownloadedHeader": { + "description": "Header label for downloaded items section in library" + }, + "queueFilteringIndicator": "Filtering...", + "@queueFilteringIndicator": { + "description": "Shown while filter results are being computed" + }, + "queueTrackCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@queueTrackCount": { + "description": "Track count label with plural support", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueAlbumCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@queueAlbumCount": { + "description": "Album count label with plural support", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueEmptyAlbums": "No album downloads", + "@queueEmptyAlbums": { + "description": "Empty state title when no album downloads exist" + }, + "queueEmptyAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@queueEmptyAlbumsSubtitle": { + "description": "Empty state subtitle for album downloads" + }, + "queueEmptySingles": "No single downloads", + "@queueEmptySingles": { + "description": "Empty state title when no single track downloads exist" + }, + "queueEmptySinglesSubtitle": "Single track downloads will appear here", + "@queueEmptySinglesSubtitle": { + "description": "Empty state subtitle for single track downloads" + }, + "queueEmptyHistory": "No download history", + "@queueEmptyHistory": { + "description": "Empty state title when download history is empty" + }, + "queueEmptyHistorySubtitle": "Downloaded tracks will appear here", + "@queueEmptyHistorySubtitle": { + "description": "Empty state subtitle for download history" + }, + "selectionAllPlaylistsSelected": "All playlists selected", + "@selectionAllPlaylistsSelected": { + "description": "Shown when all playlists are selected in selection mode" + }, + "selectionTapPlaylistsToSelect": "Tap playlists to select", + "@selectionTapPlaylistsToSelect": { + "description": "Hint shown in playlist selection mode" + }, + "selectionSelectPlaylistsToDelete": "Select playlists to delete", + "@selectionSelectPlaylistsToDelete": { + "description": "Hint shown when no playlists are selected for deletion" + }, + "audioAnalysisTitle": "Audio Quality Analysis", + "@audioAnalysisTitle": { + "description": "Title for audio analysis section" + }, + "audioAnalysisDescription": "Verify lossless quality with spectrum analysis", + "@audioAnalysisDescription": { + "description": "Description for audio analysis tap-to-analyze prompt" + }, + "audioAnalysisAnalyzing": "Analyzing audio...", + "@audioAnalysisAnalyzing": { + "description": "Loading text while analyzing audio" + }, + "audioAnalysisSampleRate": "Sample Rate", + "@audioAnalysisSampleRate": { + "description": "Sample rate metric label" + }, + "audioAnalysisBitDepth": "Bit Depth", + "@audioAnalysisBitDepth": { + "description": "Bit depth metric label" + }, + "audioAnalysisChannels": "Channels", + "@audioAnalysisChannels": { + "description": "Channels metric label" + }, + "audioAnalysisDuration": "Duration", + "@audioAnalysisDuration": { + "description": "Duration metric label" + }, + "audioAnalysisNyquist": "Nyquist", + "@audioAnalysisNyquist": { + "description": "Nyquist frequency metric label" + }, + "audioAnalysisFileSize": "Size", + "@audioAnalysisFileSize": { + "description": "File size metric label" + }, + "audioAnalysisDynamicRange": "Dynamic Range", + "@audioAnalysisDynamicRange": { + "description": "Dynamic range metric label" + }, + "audioAnalysisPeak": "Peak", + "@audioAnalysisPeak": { + "description": "Peak amplitude metric label" + }, + "audioAnalysisRms": "RMS", + "@audioAnalysisRms": { + "description": "RMS level metric label" + }, + "audioAnalysisSamples": "Samples", + "@audioAnalysisSamples": { + "description": "Total samples metric label" + }, + "extensionsSearchWith": "Search with {providerName}", + "@extensionsSearchWith": { + "description": "Extensions page - subtitle for built-in search provider option", + "placeholders": { + "providerName": { + "type": "String" + } + } + }, + "extensionsHomeFeedProvider": "Home Feed Provider", + "@extensionsHomeFeedProvider": { + "description": "Extensions page - label for home feed provider selector" + }, + "extensionsHomeFeedDescription": "Choose which extension provides the home feed on the main screen", + "@extensionsHomeFeedDescription": { + "description": "Extensions page - description for home feed provider picker" + }, + "extensionsHomeFeedAuto": "Auto", + "@extensionsHomeFeedAuto": { + "description": "Label for auto-selected search provider" + }, + "extensionsHomeFeedAutoSubtitle": "Automatically select the best available", + "@extensionsHomeFeedAutoSubtitle": { + "description": "Extensions page - subtitle for auto home feed option" + }, + "extensionsHomeFeedUse": "Use {extensionName} home feed", + "@extensionsHomeFeedUse": { + "description": "Extensions page - subtitle for a specific extension home feed option", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "extensionsNoHomeFeedExtensions": "No extensions with home feed", + "@extensionsNoHomeFeedExtensions": { + "description": "Extensions page - shown when no installed extension has home feed" + }, + "sortAlphaAsc": "A-Z", + "@sortAlphaAsc": { + "description": "Sort option - alphabetical ascending" + }, + "sortAlphaDesc": "Z-A", + "@sortAlphaDesc": { + "description": "Sort option - alphabetical descending" + }, + "cancelDownloadTitle": "Cancel download?", + "@cancelDownloadTitle": { + "description": "Dialog title when confirming cancellation of an active download" + }, + "cancelDownloadContent": "This will cancel the active download for \"{trackName}\".", + "@cancelDownloadContent": { + "description": "Dialog body when confirming cancellation of an active download", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "cancelDownloadKeep": "Keep", + "@cancelDownloadKeep": { + "description": "Dialog button - keep the active download (do not cancel)" + }, + "metadataSaveFailedFfmpeg": "Failed to save metadata via FFmpeg", + "@metadataSaveFailedFfmpeg": { + "description": "Snackbar error when FFmpeg fails to write metadata" + }, + "metadataSaveFailedStorage": "Failed to write metadata back to storage", + "@metadataSaveFailedStorage": { + "description": "Snackbar error when writing metadata file back to storage fails" + }, + "snackbarFolderPickerFailed": "Failed to open folder picker: {error}", + "@snackbarFolderPickerFailed": { + "description": "Snackbar shown when folder picker fails to open", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "errorLoadAlbum": "Failed to load album", + "@errorLoadAlbum": { + "description": "Error state shown when album fails to load" + }, + "errorLoadPlaylist": "Failed to load playlist", + "@errorLoadPlaylist": { + "description": "Error state shown when playlist fails to load" + }, + "errorLoadArtist": "Failed to load artist", + "@errorLoadArtist": { + "description": "Error state shown when artist fails to load" + }, + "notifChannelDownloadName": "Download Progress", + "@notifChannelDownloadName": { + "description": "Android notification channel name for download progress" + }, + "notifChannelDownloadDesc": "Shows download progress for tracks", + "@notifChannelDownloadDesc": { + "description": "Android notification channel description for download progress" + }, + "notifChannelLibraryScanName": "Library Scan", + "@notifChannelLibraryScanName": { + "description": "Android notification channel name for library scan" + }, + "notifChannelLibraryScanDesc": "Shows local library scan progress", + "@notifChannelLibraryScanDesc": { + "description": "Android notification channel description for library scan" + }, + "notifDownloadingTrack": "Downloading {trackName}", + "@notifDownloadingTrack": { + "description": "Notification title while downloading a track", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "notifFinalizingTrack": "Finalizing {trackName}", + "@notifFinalizingTrack": { + "description": "Notification title while finalizing (embedding metadata) a track", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "notifEmbeddingMetadata": "Embedding metadata...", + "@notifEmbeddingMetadata": { + "description": "Notification body while embedding metadata into a downloaded track" + }, + "notifAlreadyInLibraryCount": "Already in Library ({completed}/{total})", + "@notifAlreadyInLibraryCount": { + "description": "Notification title when track is already in library, with count", + "placeholders": { + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "notifAlreadyInLibrary": "Already in Library", + "@notifAlreadyInLibrary": { + "description": "Notification title when track is already in library" + }, + "notifDownloadCompleteCount": "Download Complete ({completed}/{total})", + "@notifDownloadCompleteCount": { + "description": "Notification title when download is complete, with count", + "placeholders": { + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "notifDownloadComplete": "Download Complete", + "@notifDownloadComplete": { + "description": "Notification title when a single download is complete" + }, + "notifDownloadsFinished": "Downloads Finished ({completed} done, {failed} failed)", + "@notifDownloadsFinished": { + "description": "Notification title when queue finishes with some failures", + "placeholders": { + "completed": { + "type": "int" + }, + "failed": { + "type": "int" + } + } + }, + "notifAllDownloadsComplete": "All Downloads Complete", + "@notifAllDownloadsComplete": { + "description": "Notification title when all downloads finish successfully" + }, + "notifTracksDownloadedSuccess": "{count} tracks downloaded successfully", + "@notifTracksDownloadedSuccess": { + "description": "Notification body for queue complete - how many tracks were downloaded", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifScanningLibrary": "Scanning local library", + "@notifScanningLibrary": { + "description": "Notification title while scanning local library" + }, + "notifLibraryScanProgressWithTotal": "{scanned}/{total} files • {percentage}%", + "@notifLibraryScanProgressWithTotal": { + "description": "Notification body for library scan progress when total is known", + "placeholders": { + "scanned": { + "type": "int" + }, + "total": { + "type": "int" + }, + "percentage": { + "type": "int" + } + } + }, + "notifLibraryScanProgressNoTotal": "{scanned} files scanned • {percentage}%", + "@notifLibraryScanProgressNoTotal": { + "description": "Notification body for library scan progress when total is unknown", + "placeholders": { + "scanned": { + "type": "int" + }, + "percentage": { + "type": "int" + } + } + }, + "notifLibraryScanComplete": "Library scan complete", + "@notifLibraryScanComplete": { + "description": "Notification title when library scan finishes" + }, + "notifLibraryScanCompleteBody": "{count} tracks indexed", + "@notifLibraryScanCompleteBody": { + "description": "Notification body for library scan complete - number of indexed tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanExcluded": "{count} excluded", + "@notifLibraryScanExcluded": { + "description": "Library scan complete suffix - excluded track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanErrors": "{count} errors", + "@notifLibraryScanErrors": { + "description": "Library scan complete suffix - error count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanFailed": "Library scan failed", + "@notifLibraryScanFailed": { + "description": "Notification title when library scan fails" + }, + "notifLibraryScanCancelled": "Library scan cancelled", + "@notifLibraryScanCancelled": { + "description": "Notification title when library scan is cancelled by the user" + }, + "notifLibraryScanStopped": "Scan stopped before completion.", + "@notifLibraryScanStopped": { + "description": "Notification body when library scan is cancelled" + }, + "notifDownloadingUpdate": "Downloading SpotiFLAC v{version}", + "@notifDownloadingUpdate": { + "description": "Notification title while downloading an app update", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "notifUpdateProgress": "{received} / {total} MB • {percentage}%", + "@notifUpdateProgress": { + "description": "Notification body showing update download progress", + "placeholders": { + "received": { + "type": "String" + }, + "total": { + "type": "String" + }, + "percentage": { + "type": "int" + } + } + }, + "notifUpdateReady": "Update Ready", + "@notifUpdateReady": { + "description": "Notification title when app update download is complete" + }, + "notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.", + "@notifUpdateReadyBody": { + "description": "Notification body when app update is ready to install", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "notifUpdateFailed": "Update Failed", + "@notifUpdateFailed": { + "description": "Notification title when app update download fails" + }, + "notifUpdateFailedBody": "Could not download update. Try again later.", + "@notifUpdateFailedBody": { + "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } diff --git a/lib/l10n/arb/app_es_ES.arb b/lib/l10n/arb/app_es_ES.arb index 286f0a0c..7fccb158 100644 --- a/lib/l10n/arb/app_es_ES.arb +++ b/lib/l10n/arb/app_es_ES.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index dccad293..ab865785 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_hi.arb b/lib/l10n/arb/app_hi.arb index 355c5c7e..7935892f 100644 --- a/lib/l10n/arb/app_hi.arb +++ b/lib/l10n/arb/app_hi.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index 667c7d8f..6b0733dd 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb index c265d491..75f9e1b0 100644 --- a/lib/l10n/arb/app_ja.arb +++ b/lib/l10n/arb/app_ja.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_ko.arb b/lib/l10n/arb/app_ko.arb index 862b39cd..398c09b5 100644 --- a/lib/l10n/arb/app_ko.arb +++ b/lib/l10n/arb/app_ko.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 5e4be112..b9cbaa09 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 1f41f8f7..de1ff199 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -1728,5 +1728,2858 @@ "type": "int" } } + }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, + "historySearchHint": "Search history...", + "@historySearchHint": { + "description": "Search bar placeholder in history" + }, + "downloadSingleFilenameFormat": "Single Filename Format", + "@downloadSingleFilenameFormat": { + "description": "Setting for output filename pattern for singles/EPs" + }, + "downloadSingleFilenameFormatDescription": "Filename pattern for singles and EPs. Uses the same tags as the album format.", + "@downloadSingleFilenameFormatDescription": { + "description": "Subtitle description for single filename format setting" + }, + "optionsDefaultSearchTab": "Default Search Tab", + "@optionsDefaultSearchTab": { + "description": "Title for the preferred default search tab setting" + }, + "optionsDefaultSearchTabSubtitle": "Choose which tab opens first for new search results.", + "@optionsDefaultSearchTabSubtitle": { + "description": "Subtitle for the preferred default search tab setting" + }, + "optionsReplayGain": "ReplayGain", + "@optionsReplayGain": { + "description": "Title for ReplayGain setting toggle" + }, + "optionsReplayGainSubtitleOn": "Scan loudness and embed ReplayGain tags (EBU R128)", + "@optionsReplayGainSubtitleOn": { + "description": "Subtitle when ReplayGain is enabled" + }, + "optionsReplayGainSubtitleOff": "Disabled: no loudness normalization tags", + "@optionsReplayGainSubtitleOff": { + "description": "Subtitle when ReplayGain is disabled" + }, + "optionsArtistTagMode": "Artist Tag Mode", + "@optionsArtistTagMode": { + "description": "Setting title for how artist metadata is written into files" + }, + "optionsArtistTagModeDescription": "Choose how multiple artists are written into embedded tags.", + "@optionsArtistTagModeDescription": { + "description": "Bottom-sheet description for artist tag mode setting" + }, + "optionsArtistTagModeJoined": "Single joined value", + "@optionsArtistTagModeJoined": { + "description": "Artist tag mode option that joins multiple artists into one value" + }, + "optionsArtistTagModeJoinedSubtitle": "Write one ARTIST value like \"Artist A, Artist B\" for maximum player compatibility.", + "@optionsArtistTagModeJoinedSubtitle": { + "description": "Subtitle for joined artist tag mode" + }, + "optionsArtistTagModeSplitVorbis": "Split tags for FLAC/Opus", + "@optionsArtistTagModeSplitVorbis": { + "description": "Artist tag mode option that writes repeated ARTIST tags for Vorbis formats" + }, + "optionsArtistTagModeSplitVorbisSubtitle": "Write one artist tag per artist for FLAC and Opus; MP3 and M4A stay joined.", + "@optionsArtistTagModeSplitVorbisSubtitle": { + "description": "Subtitle for split Vorbis artist tag mode" + }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, + "aboutTranslators": "Translators", + "@aboutTranslators": { + "description": "Section for translators" + }, + "aboutTelegramChannel": "Telegram Channel", + "@aboutTelegramChannel": { + "description": "Link to Telegram channel" + }, + "aboutTelegramChannelSubtitle": "Announcements and updates", + "@aboutTelegramChannelSubtitle": { + "description": "Subtitle for Telegram channel" + }, + "aboutTelegramChat": "Telegram Community", + "@aboutTelegramChat": { + "description": "Link to Telegram chat group" + }, + "aboutTelegramChatSubtitle": "Chat with other users", + "@aboutTelegramChatSubtitle": { + "description": "Subtitle for Telegram chat" + }, + "aboutSocial": "Social", + "@aboutSocial": { + "description": "Section for social links" + }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, + "artistPopular": "Popular", + "@artistPopular": { + "description": "Section header for popular/top tracks" + }, + "artistMonthlyListeners": "{count} monthly listeners", + "@artistMonthlyListeners": { + "description": "Monthly listener count display", + "placeholders": { + "count": { + "type": "String", + "description": "Formatted listener count" + } + } + }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, + "dialogDownload": "Download", + "@dialogDownload": { + "description": "Confirm button in Download All dialog" + }, + "csvImportTracks": "{count} tracks from CSV", + "@csvImportTracks": { + "description": "Label shown in quality picker for CSV import", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "errorUrlNotRecognized": "Link not recognized", + "@errorUrlNotRecognized": { + "description": "Error title - URL not handled by any extension or service" + }, + "errorUrlNotRecognizedMessage": "This link is not supported. Make sure the URL is correct and a compatible extension is installed.", + "@errorUrlNotRecognizedMessage": { + "description": "Error message - URL not recognized explanation" + }, + "errorUrlFetchFailed": "Failed to load content from this link. Please try again.", + "@errorUrlFetchFailed": { + "description": "Error message - generic URL fetch failure" + }, + "searchSortTitle": "Sort Results", + "@searchSortTitle": { + "description": "Bottom sheet title for search sort options" + }, + "searchSortDefault": "Default", + "@searchSortDefault": { + "description": "Sort option - default API order" + }, + "searchSortTitleAZ": "Title (A-Z)", + "@searchSortTitleAZ": { + "description": "Sort option - title ascending" + }, + "searchSortTitleZA": "Title (Z-A)", + "@searchSortTitleZA": { + "description": "Sort option - title descending" + }, + "searchSortArtistAZ": "Artist (A-Z)", + "@searchSortArtistAZ": { + "description": "Sort option - artist ascending" + }, + "searchSortArtistZA": "Artist (Z-A)", + "@searchSortArtistZA": { + "description": "Sort option - artist descending" + }, + "searchSortDurationShort": "Duration (Shortest)", + "@searchSortDurationShort": { + "description": "Sort option - shortest duration first" + }, + "searchSortDurationLong": "Duration (Longest)", + "@searchSortDurationLong": { + "description": "Sort option - longest duration first" + }, + "searchSortDateOldest": "Release Date (Oldest)", + "@searchSortDateOldest": { + "description": "Sort option - oldest release first" + }, + "searchSortDateNewest": "Release Date (Newest)", + "@searchSortDateNewest": { + "description": "Sort option - newest release first" + }, + "filenameShowAdvancedTags": "Show advanced tags", + "@filenameShowAdvancedTags": { + "description": "Toggle label for showing advanced filename tags" + }, + "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns", + "@filenameShowAdvancedTagsDescription": { + "description": "Description for advanced filename tag toggle" + }, + "folderOrganizationByPlaylist": "By Playlist", + "@folderOrganizationByPlaylist": { + "description": "Folder option - playlist folders" + }, + "folderOrganizationByPlaylistSubtitle": "Separate folder for each playlist", + "@folderOrganizationByPlaylistSubtitle": { + "description": "Subtitle for playlist folder option" + }, + "providerPriorityFallbackExtensionsTitle": "Extension Fallback", + "@providerPriorityFallbackExtensionsTitle": { + "description": "Section title for choosing which download extensions can be used as fallback providers" + }, + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "@providerPriorityFallbackExtensionsDescription": { + "description": "Section description for extension fallback selection" + }, + "providerPriorityFallbackExtensionsHint": "Only enabled extensions with download-provider capability are listed here.", + "@providerPriorityFallbackExtensionsHint": { + "description": "Hint below the extension fallback selection list" + }, + "sectionLyrics": "Lyrics", + "@sectionLyrics": { + "description": "Settings section header" + }, + "lyricsMode": "Lyrics Mode", + "@lyricsMode": { + "description": "Setting - how to save lyrics" + }, + "lyricsModeDescription": "Choose how lyrics are saved with your downloads", + "@lyricsModeDescription": { + "description": "Lyrics mode picker description" + }, + "lyricsModeEmbed": "Embed in file", + "@lyricsModeEmbed": { + "description": "Lyrics mode option - embed in audio file" + }, + "lyricsModeEmbedSubtitle": "Lyrics stored inside FLAC metadata", + "@lyricsModeEmbedSubtitle": { + "description": "Subtitle for embed option" + }, + "lyricsModeExternal": "External .lrc file", + "@lyricsModeExternal": { + "description": "Lyrics mode option - separate LRC file" + }, + "lyricsModeExternalSubtitle": "Separate .lrc file for players like Samsung Music", + "@lyricsModeExternalSubtitle": { + "description": "Subtitle for external option" + }, + "lyricsModeBoth": "Both", + "@lyricsModeBoth": { + "description": "Lyrics mode option - embed and external" + }, + "lyricsModeBothSubtitle": "Embed and save .lrc file", + "@lyricsModeBothSubtitle": { + "description": "Subtitle for both option" + }, + "trackGenre": "Genre", + "@trackGenre": { + "description": "Metadata label - music genre" + }, + "trackLabel": "Label", + "@trackLabel": { + "description": "Metadata label - record label" + }, + "trackCopyright": "Copyright", + "@trackCopyright": { + "description": "Metadata label - copyright information" + }, + "trackLyricsNotInFile": "No lyrics found in this file", + "@trackLyricsNotInFile": { + "description": "Message when no embedded lyrics in audio file" + }, + "trackFetchOnlineLyrics": "Fetch from Online", + "@trackFetchOnlineLyrics": { + "description": "Action - fetch lyrics from online providers" + }, + "trackEmbedLyrics": "Embed Lyrics", + "@trackEmbedLyrics": { + "description": "Action - embed lyrics into audio file" + }, + "trackLyricsEmbedded": "Lyrics embedded successfully", + "@trackLyricsEmbedded": { + "description": "Snackbar - lyrics saved to file" + }, + "trackInstrumental": "Instrumental track", + "@trackInstrumental": { + "description": "Message when track is instrumental (no lyrics)" + }, + "storeAddRepoTitle": "Add Extension Repository", + "@storeAddRepoTitle": { + "description": "Store setup screen - heading when no repo is configured" + }, + "storeAddRepoDescription": "Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.", + "@storeAddRepoDescription": { + "description": "Store setup screen - explanatory text" + }, + "storeRepoUrlLabel": "Repository URL", + "@storeRepoUrlLabel": { + "description": "Label for the repository URL input field" + }, + "storeRepoUrlHint": "https://github.com/user/repo", + "@storeRepoUrlHint": { + "description": "Hint/placeholder for the repository URL input field" + }, + "storeRepoUrlHelper": "e.g. https://github.com/user/extensions-repo", + "@storeRepoUrlHelper": { + "description": "Helper text below the repository URL input field" + }, + "storeAddRepoButton": "Add Repository", + "@storeAddRepoButton": { + "description": "Button to submit a new repository URL" + }, + "storeChangeRepoTooltip": "Change repository", + "@storeChangeRepoTooltip": { + "description": "Tooltip for the change-repository icon button in the app bar" + }, + "storeRepoDialogTitle": "Extension Repository", + "@storeRepoDialogTitle": { + "description": "Title of the change/remove repository dialog" + }, + "storeRepoDialogCurrent": "Current repository:", + "@storeRepoDialogCurrent": { + "description": "Label shown above the current repository URL in the dialog" + }, + "storeNewRepoUrlLabel": "New Repository URL", + "@storeNewRepoUrlLabel": { + "description": "Label for the new repository URL field inside the dialog" + }, + "storeLoadError": "Failed to load repository", + "@storeLoadError": { + "description": "Error heading when the store cannot be loaded" + }, + "storeEmptyNoExtensions": "No extensions available", + "@storeEmptyNoExtensions": { + "description": "Message when store has no extensions" + }, + "storeEmptyNoResults": "No extensions found", + "@storeEmptyNoResults": { + "description": "Message when search/filter returns no results" + }, + "extensionsFallbackTitle": "Fallback Extensions", + "@extensionsFallbackTitle": { + "description": "Setting and page title for choosing which download extensions can be used during fallback" + }, + "extensionsFallbackSubtitle": "Choose which installed download extensions can be used as fallback", + "@extensionsFallbackSubtitle": { + "description": "Subtitle for download fallback extensions menu" + }, + "downloadLossy320": "Lossy 320kbps", + "@downloadLossy320": { + "description": "Quality option label for Tidal lossy 320kbps" + }, + "downloadLossyFormat": "Lossy Format", + "@downloadLossyFormat": { + "description": "Setting title to pick output format for Tidal lossy downloads" + }, + "downloadLossy320Format": "Lossy 320kbps Format", + "@downloadLossy320Format": { + "description": "Title of the Tidal lossy format picker bottom sheet" + }, + "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", + "@downloadLossy320FormatDesc": { + "description": "Description in the Tidal lossy format picker" + }, + "downloadLossyMp3": "MP3 320kbps", + "@downloadLossyMp3": { + "description": "Tidal lossy format option - MP3 320kbps" + }, + "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", + "@downloadLossyMp3Subtitle": { + "description": "Subtitle for MP3 320kbps Tidal lossy option" + }, + "downloadLossyOpus256": "Opus 256kbps", + "@downloadLossyOpus256": { + "description": "Tidal lossy format option - Opus 256kbps" + }, + "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", + "@downloadLossyOpus256Subtitle": { + "description": "Subtitle for Opus 256kbps Tidal lossy option" + }, + "downloadLossyOpus128": "Opus 128kbps", + "@downloadLossyOpus128": { + "description": "Tidal lossy format option - Opus 128kbps" + }, + "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", + "@downloadLossyOpus128Subtitle": { + "description": "Subtitle for Opus 128kbps Tidal lossy option" + }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, + "albumFolderArtistAlbumSingles": "Artist / Album + Singles", + "@albumFolderArtistAlbumSingles": { + "description": "Album folder option with singles inside artist" + }, + "albumFolderArtistAlbumSinglesSubtitle": "Artist/Album/ and Artist/Singles/", + "@albumFolderArtistAlbumSinglesSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistAlbumFlat": "Artist / Album (Singles flat)", + "@albumFolderArtistAlbumFlat": { + "description": "Album folder option with singles directly in artist folder" + }, + "albumFolderArtistAlbumFlatSubtitle": "Artist/Album/ and Artist/song.flac", + "@albumFolderArtistAlbumFlatSubtitle": { + "description": "Folder structure example for flat singles" + }, + "downloadedAlbumDiscHeader": "Disc {discNumber}", + "@downloadedAlbumDiscHeader": { + "description": "Header for disc separator in multi-disc albums", + "placeholders": { + "discNumber": { + "type": "int", + "example": "1" + } + } + }, + "recentTypeArtist": "Artist", + "@recentTypeArtist": { + "description": "Recent access item type - artist" + }, + "recentTypeAlbum": "Album", + "@recentTypeAlbum": { + "description": "Recent access item type - album" + }, + "recentTypeSong": "Song", + "@recentTypeSong": { + "description": "Recent access item type - song/track" + }, + "recentTypePlaylist": "Playlist", + "@recentTypePlaylist": { + "description": "Recent access item type - playlist" + }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, + "recentPlaylistInfo": "Playlist: {name}", + "@recentPlaylistInfo": { + "description": "Snackbar message when tapping playlist in recent access", + "placeholders": { + "name": { + "type": "String", + "description": "Playlist name" + } + } + }, + "discographyDownload": "Download Discography", + "@discographyDownload": { + "description": "Button - download artist discography" + }, + "discographyDownloadAll": "Download All", + "@discographyDownloadAll": { + "description": "Option - download entire discography" + }, + "discographyDownloadAllSubtitle": "{count} tracks from {albumCount} releases", + "@discographyDownloadAllSubtitle": { + "description": "Subtitle showing total tracks and albums", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographyAlbumsOnly": "Albums Only", + "@discographyAlbumsOnly": { + "description": "Option - download only albums" + }, + "discographyAlbumsOnlySubtitle": "{count} tracks from {albumCount} albums", + "@discographyAlbumsOnlySubtitle": { + "description": "Subtitle showing album tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySinglesOnly": "Singles & EPs Only", + "@discographySinglesOnly": { + "description": "Option - download only singles" + }, + "discographySinglesOnlySubtitle": "{count} tracks from {albumCount} singles", + "@discographySinglesOnlySubtitle": { + "description": "Subtitle showing singles tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySelectAlbums": "Select Albums...", + "@discographySelectAlbums": { + "description": "Option - manually select albums to download" + }, + "discographySelectAlbumsSubtitle": "Choose specific albums or singles", + "@discographySelectAlbumsSubtitle": { + "description": "Subtitle for select albums option" + }, + "discographyFetchingTracks": "Fetching tracks...", + "@discographyFetchingTracks": { + "description": "Progress - fetching album tracks" + }, + "discographyFetchingAlbum": "Fetching {current} of {total}...", + "@discographyFetchingAlbum": { + "description": "Progress - fetching specific album", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "discographySelectedCount": "{count} selected", + "@discographySelectedCount": { + "description": "Selection count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographyDownloadSelected": "Download Selected", + "@discographyDownloadSelected": { + "description": "Button - download selected albums" + }, + "discographyAddedToQueue": "Added {count} tracks to queue", + "@discographyAddedToQueue": { + "description": "Snackbar - tracks added from discography", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographySkippedDownloaded": "{added} added, {skipped} already downloaded", + "@discographySkippedDownloaded": { + "description": "Snackbar - with skipped tracks count", + "placeholders": { + "added": { + "type": "int" + }, + "skipped": { + "type": "int" + } + } + }, + "discographyNoAlbums": "No albums available", + "@discographyNoAlbums": { + "description": "Error - no albums found for artist" + }, + "discographyFailedToFetch": "Failed to fetch some albums", + "@discographyFailedToFetch": { + "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryAutoScan": "Auto Scan", + "@libraryAutoScan": { + "description": "Setting for automatic library scanning" + }, + "libraryAutoScanSubtitle": "Automatically scan your library for new files", + "@libraryAutoScanSubtitle": { + "description": "Subtitle for auto scan setting" + }, + "libraryAutoScanOff": "Off", + "@libraryAutoScanOff": { + "description": "Auto scan disabled" + }, + "libraryAutoScanOnOpen": "Every app open", + "@libraryAutoScanOnOpen": { + "description": "Auto scan when app opens" + }, + "libraryAutoScanDaily": "Daily", + "@libraryAutoScanDaily": { + "description": "Auto scan once per day" + }, + "libraryAutoScanWeekly": "Weekly", + "@libraryAutoScanWeekly": { + "description": "Auto scan once per week" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}", + "@libraryTracksUnit": { + "description": "Unit label for tracks count (without the number itself)", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryFilesUnit": "{count, plural, =1{file} other{files}}", + "@libraryFilesUnit": { + "description": "Unit label for files count during library scanning", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanFinalizing": "Finalizing library...", + "@libraryScanFinalizing": { + "description": "Status shown after file scanning finishes but library persistence is still running" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterMetadata": "Metadata", + "@libraryFilterMetadata": { + "description": "Filter section - metadata completeness" + }, + "libraryFilterMetadataComplete": "Complete metadata", + "@libraryFilterMetadataComplete": { + "description": "Filter option - items with complete metadata" + }, + "libraryFilterMetadataMissingAny": "Missing any metadata", + "@libraryFilterMetadataMissingAny": { + "description": "Filter option - items missing any tracked metadata field" + }, + "libraryFilterMetadataMissingYear": "Missing year", + "@libraryFilterMetadataMissingYear": { + "description": "Filter option - items missing release year/date" + }, + "libraryFilterMetadataMissingGenre": "Missing genre", + "@libraryFilterMetadataMissingGenre": { + "description": "Filter option - items missing genre" + }, + "libraryFilterMetadataMissingAlbumArtist": "Missing album artist", + "@libraryFilterMetadataMissingAlbumArtist": { + "description": "Filter option - items missing album artist" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterSortAlbumAsc": "Album (A-Z)", + "@libraryFilterSortAlbumAsc": { + "description": "Sort option - album ascending" + }, + "libraryFilterSortAlbumDesc": "Album (Z-A)", + "@libraryFilterSortAlbumDesc": { + "description": "Sort option - album descending" + }, + "libraryFilterSortGenreAsc": "Genre (A-Z)", + "@libraryFilterSortGenreAsc": { + "description": "Sort option - genre ascending" + }, + "libraryFilterSortGenreDesc": "Genre (Z-A)", + "@libraryFilterSortGenreDesc": { + "description": "Sort option - genre descending" + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Deezer", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Repo tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackReEnrichFieldsTitle": "Fields to update", + "@trackReEnrichFieldsTitle": { + "description": "Section title for field selection in re-enrich dialog" + }, + "trackReEnrichFieldCover": "Cover Art", + "@trackReEnrichFieldCover": { + "description": "Checkbox label for cover art field in re-enrich" + }, + "trackReEnrichFieldLyrics": "Lyrics", + "@trackReEnrichFieldLyrics": { + "description": "Checkbox label for lyrics field in re-enrich" + }, + "trackReEnrichFieldBasicTags": "Album, Album Artist", + "@trackReEnrichFieldBasicTags": { + "description": "Checkbox label for basic tags in re-enrich (title/artist are never overwritten)" + }, + "trackReEnrichFieldTrackInfo": "Track & Disc Number", + "@trackReEnrichFieldTrackInfo": { + "description": "Checkbox label for track info in re-enrich" + }, + "trackReEnrichFieldReleaseInfo": "Date & ISRC", + "@trackReEnrichFieldReleaseInfo": { + "description": "Checkbox label for release info in re-enrich" + }, + "trackReEnrichFieldExtra": "Genre, Label, Copyright", + "@trackReEnrichFieldExtra": { + "description": "Checkbox label for extra metadata in re-enrich" + }, + "trackReEnrichSelectAll": "Select All", + "@trackReEnrichSelectAll": { + "description": "Select all fields checkbox in re-enrich" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "queueFlacAction": "Queue FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Finding FLAC matches... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "No reliable online matches found for the selection", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Added {addedCount} tracks to queue, skipped {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3, Opus, ALAC, or FLAC", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConfirmMessageLossless": "Convert from {sourceFormat} to {targetFormat}? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless-to-lossless conversion", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + } + } + }, + "trackConvertLosslessHint": "Lossless conversion — no quality loss", + "@trackConvertLosslessHint": { + "description": "Hint shown when converting between lossless formats" + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" + }, + "cueSplitTitle": "Split CUE Sheet", + "@cueSplitTitle": { + "description": "Title for CUE split bottom sheet" + }, + "cueSplitSubtitle": "Split CUE+FLAC into individual tracks", + "@cueSplitSubtitle": { + "description": "Subtitle for CUE split menu item" + }, + "cueSplitAlbum": "Album: {album}", + "@cueSplitAlbum": { + "description": "Album name in CUE split sheet", + "placeholders": { + "album": { + "type": "String" + } + } + }, + "cueSplitArtist": "Artist: {artist}", + "@cueSplitArtist": { + "description": "Artist name in CUE split sheet", + "placeholders": { + "artist": { + "type": "String" + } + } + }, + "cueSplitTrackCount": "{count} tracks", + "@cueSplitTrackCount": { + "description": "Number of tracks in CUE sheet", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitConfirmTitle": "Split CUE Album", + "@cueSplitConfirmTitle": { + "description": "CUE split confirmation dialog title" + }, + "cueSplitConfirmMessage": "Split \"{album}\" into {count} individual FLAC files?\n\nFiles will be saved to the same directory.", + "@cueSplitConfirmMessage": { + "description": "CUE split confirmation dialog message", + "placeholders": { + "album": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})", + "@cueSplitSplitting": { + "description": "Snackbar while splitting CUE", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "cueSplitSuccess": "Split into {count} tracks successfully", + "@cueSplitSuccess": { + "description": "Snackbar after successful CUE split", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitFailed": "CUE split failed", + "@cueSplitFailed": { + "description": "Snackbar when CUE split fails" + }, + "cueSplitNoAudioFile": "Audio file not found for this CUE sheet", + "@cueSplitNoAudioFile": { + "description": "Error when CUE audio file is missing" + }, + "cueSplitButton": "Split into Tracks", + "@cueSplitButton": { + "description": "Button text to start CUE splitting" + }, + "actionCreate": "Create", + "@actionCreate": { + "description": "Generic action button - create" + }, + "collectionFoldersTitle": "My folders", + "@collectionFoldersTitle": { + "description": "Library section title for custom folders" + }, + "collectionWishlist": "Wishlist", + "@collectionWishlist": { + "description": "Custom folder for saved tracks to download later" + }, + "collectionLoved": "Loved", + "@collectionLoved": { + "description": "Custom folder for favorite tracks" + }, + "collectionPlaylists": "Playlists", + "@collectionPlaylists": { + "description": "Custom user playlists folder" + }, + "collectionPlaylist": "Playlist", + "@collectionPlaylist": { + "description": "Single playlist label" + }, + "collectionAddToPlaylist": "Add to playlist", + "@collectionAddToPlaylist": { + "description": "Action to add a track to user playlist" + }, + "collectionCreatePlaylist": "Create playlist", + "@collectionCreatePlaylist": { + "description": "Action to create a new playlist" + }, + "collectionNoPlaylistsYet": "No playlists yet", + "@collectionNoPlaylistsYet": { + "description": "Empty state title when user has no playlists" + }, + "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks", + "@collectionNoPlaylistsSubtitle": { + "description": "Empty state subtitle when user has no playlists" + }, + "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@collectionPlaylistTracks": { + "description": "Track count label for custom playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "collectionAddedToPlaylist": "Added to \"{playlistName}\"", + "@collectionAddedToPlaylist": { + "description": "Snackbar after adding track to playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"", + "@collectionAlreadyInPlaylist": { + "description": "Snackbar when track already exists in playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionPlaylistCreated": "Playlist created", + "@collectionPlaylistCreated": { + "description": "Snackbar after creating playlist" + }, + "collectionPlaylistNameHint": "Playlist name", + "@collectionPlaylistNameHint": { + "description": "Hint text for playlist name input" + }, + "collectionPlaylistNameRequired": "Playlist name is required", + "@collectionPlaylistNameRequired": { + "description": "Validation error for empty playlist name" + }, + "collectionRenamePlaylist": "Rename playlist", + "@collectionRenamePlaylist": { + "description": "Action to rename playlist" + }, + "collectionDeletePlaylist": "Delete playlist", + "@collectionDeletePlaylist": { + "description": "Action to delete playlist" + }, + "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?", + "@collectionDeletePlaylistMessage": { + "description": "Confirmation message for deleting playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionPlaylistDeleted": "Playlist deleted", + "@collectionPlaylistDeleted": { + "description": "Snackbar after deleting playlist" + }, + "collectionPlaylistRenamed": "Playlist renamed", + "@collectionPlaylistRenamed": { + "description": "Snackbar after renaming playlist" + }, + "collectionWishlistEmptyTitle": "Wishlist is empty", + "@collectionWishlistEmptyTitle": { + "description": "Wishlist empty state title" + }, + "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later", + "@collectionWishlistEmptySubtitle": { + "description": "Wishlist empty state subtitle" + }, + "collectionLovedEmptyTitle": "Loved folder is empty", + "@collectionLovedEmptyTitle": { + "description": "Loved empty state title" + }, + "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites", + "@collectionLovedEmptySubtitle": { + "description": "Loved empty state subtitle" + }, + "collectionPlaylistEmptyTitle": "Playlist is empty", + "@collectionPlaylistEmptyTitle": { + "description": "Playlist empty state title" + }, + "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here", + "@collectionPlaylistEmptySubtitle": { + "description": "Playlist empty state subtitle" + }, + "collectionRemoveFromPlaylist": "Remove from playlist", + "@collectionRemoveFromPlaylist": { + "description": "Tooltip for removing track from playlist" + }, + "collectionRemoveFromFolder": "Remove from folder", + "@collectionRemoveFromFolder": { + "description": "Tooltip for removing track from wishlist/loved folder" + }, + "collectionRemoved": "\"{trackName}\" removed", + "@collectionRemoved": { + "description": "Snackbar after removing a track from a collection", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionAddedToLoved": "\"{trackName}\" added to Loved", + "@collectionAddedToLoved": { + "description": "Snackbar after adding track to loved folder", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved", + "@collectionRemovedFromLoved": { + "description": "Snackbar after removing track from loved folder", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist", + "@collectionAddedToWishlist": { + "description": "Snackbar after adding track to wishlist", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist", + "@collectionRemovedFromWishlist": { + "description": "Snackbar after removing track from wishlist", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "trackOptionAddToLoved": "Add to Loved", + "@trackOptionAddToLoved": { + "description": "Bottom sheet action label - add track to loved folder" + }, + "trackOptionRemoveFromLoved": "Remove from Loved", + "@trackOptionRemoveFromLoved": { + "description": "Bottom sheet action label - remove track from loved folder" + }, + "trackOptionAddToWishlist": "Add to Wishlist", + "@trackOptionAddToWishlist": { + "description": "Bottom sheet action label - add track to wishlist" + }, + "trackOptionRemoveFromWishlist": "Remove from Wishlist", + "@trackOptionRemoveFromWishlist": { + "description": "Bottom sheet action label - remove track from wishlist" + }, + "collectionPlaylistChangeCover": "Change cover image", + "@collectionPlaylistChangeCover": { + "description": "Bottom sheet action to pick a custom cover image for a playlist" + }, + "collectionPlaylistRemoveCover": "Remove cover image", + "@collectionPlaylistRemoveCover": { + "description": "Bottom sheet action to remove custom cover image from a playlist" + }, + "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}", + "@selectionShareCount": { + "description": "Share button text with count in selection mode", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionShareNoFiles": "No shareable files found", + "@selectionShareNoFiles": { + "description": "Snackbar when no selected files exist on disk" + }, + "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}", + "@selectionConvertCount": { + "description": "Convert button text with count in selection mode", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionConvertNoConvertible": "No convertible tracks selected", + "@selectionConvertNoConvertible": { + "description": "Snackbar when no selected tracks support conversion" + }, + "selectionBatchConvertConfirmTitle": "Batch Convert", + "@selectionBatchConvertConfirmTitle": { + "description": "Confirmation dialog title for batch conversion" + }, + "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessage": { + "description": "Confirmation dialog message for batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "selectionBatchConvertConfirmMessageLossless": "Convert {count} {count, plural, =1{track} other{tracks}} to {format}? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, + "selectionBatchConvertProgress": "Converting {current} of {total}...", + "@selectionBatchConvertProgress": { + "description": "Snackbar during batch conversion progress", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}", + "@selectionBatchConvertSuccess": { + "description": "Snackbar after batch conversion completes", + "placeholders": { + "success": { + "type": "int" + }, + "total": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Folder named after Album Artist tag", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when album artist is used for folder names" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Folder named after Track Artist tag", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when track artist is used for folder names" + }, + "lyricsProvidersTitle": "Lyrics Provider Priority", + "@lyricsProvidersTitle": { + "description": "Settings item title for lyrics provider order" + }, + "lyricsProvidersDescription": "Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.", + "@lyricsProvidersDescription": { + "description": "Description on the lyrics provider priority page" + }, + "lyricsProvidersInfoText": "Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.", + "@lyricsProvidersInfoText": { + "description": "Info tip on lyrics provider priority page" + }, + "lyricsProvidersEnabledSection": "Enabled ({count})", + "@lyricsProvidersEnabledSection": { + "description": "Section header for enabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersDisabledSection": "Disabled ({count})", + "@lyricsProvidersDisabledSection": { + "description": "Section header for disabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersAtLeastOne": "At least one provider must remain enabled", + "@lyricsProvidersAtLeastOne": { + "description": "Snackbar when user tries to disable the last enabled provider" + }, + "lyricsProvidersSaved": "Lyrics provider priority saved", + "@lyricsProvidersSaved": { + "description": "Snackbar after saving lyrics provider priority" + }, + "lyricsProvidersDiscardContent": "You have unsaved changes that will be lost.", + "@lyricsProvidersDiscardContent": { + "description": "Body text of the discard-changes dialog on lyrics provider page" + }, + "lyricsProviderLrclibDesc": "Open-source synced lyrics database", + "@lyricsProviderLrclibDesc": { + "description": "Description for LRCLIB provider" + }, + "lyricsProviderNeteaseDesc": "NetEase Cloud Music (good for Asian songs)", + "@lyricsProviderNeteaseDesc": { + "description": "Description for Netease provider" + }, + "lyricsProviderMusixmatchDesc": "Largest lyrics database (multi-language)", + "@lyricsProviderMusixmatchDesc": { + "description": "Description for Musixmatch provider" + }, + "lyricsProviderAppleMusicDesc": "Word-by-word synced lyrics (via proxy)", + "@lyricsProviderAppleMusicDesc": { + "description": "Description for Apple Music provider" + }, + "lyricsProviderQqMusicDesc": "QQ Music (good for Chinese songs, via proxy)", + "@lyricsProviderQqMusicDesc": { + "description": "Description for QQ Music provider" + }, + "lyricsProviderExtensionDesc": "Extension provider", + "@lyricsProviderExtensionDesc": { + "description": "Generic description for extension-based lyrics providers" + }, + "safMigrationTitle": "Storage Update Required", + "@safMigrationTitle": { + "description": "Title of SAF migration dialog" + }, + "safMigrationMessage1": "SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.", + "@safMigrationMessage1": { + "description": "First paragraph of SAF migration dialog" + }, + "safMigrationMessage2": "Please select your download folder again to switch to the new storage system.", + "@safMigrationMessage2": { + "description": "Second paragraph of SAF migration dialog" + }, + "safMigrationSuccess": "Download folder updated to SAF mode", + "@safMigrationSuccess": { + "description": "Snackbar after successfully migrating to SAF" + }, + "settingsDonate": "Support Development", + "@settingsDonate": { + "description": "Settings menu item - donate page" + }, + "settingsDonateSubtitle": "Buy the developer a coffee", + "@settingsDonateSubtitle": { + "description": "Subtitle for donate menu item" + }, + "tooltipLoveAll": "Love All", + "@tooltipLoveAll": { + "description": "Tooltip for the Love All button on album/playlist screens" + }, + "tooltipAddToPlaylist": "Add to Playlist", + "@tooltipAddToPlaylist": { + "description": "Tooltip for the Add to Playlist button" + }, + "snackbarRemovedTracksFromLoved": "Removed {count} tracks from Loved", + "@snackbarRemovedTracksFromLoved": { + "description": "Snackbar after removing multiple tracks from Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedTracksToLoved": "Added {count} tracks to Loved", + "@snackbarAddedTracksToLoved": { + "description": "Snackbar after adding multiple tracks to Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogDownloadAllTitle": "Download All", + "@dialogDownloadAllTitle": { + "description": "Dialog title for bulk download confirmation" + }, + "dialogDownloadAllMessage": "Download {count} tracks?", + "@dialogDownloadAllMessage": { + "description": "Body of the Download All confirmation dialog", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "homeSkipAlreadyDownloaded": "Skip already downloaded songs", + "@homeSkipAlreadyDownloaded": { + "description": "Checkbox label in import dialog to skip already-downloaded songs" + }, + "homeGoToAlbum": "Go to Album", + "@homeGoToAlbum": { + "description": "Context menu item to navigate to the album page" + }, + "homeAlbumInfoUnavailable": "Album info not available", + "@homeAlbumInfoUnavailable": { + "description": "Snackbar when album info cannot be loaded" + }, + "snackbarLoadingCueSheet": "Loading CUE sheet...", + "@snackbarLoadingCueSheet": { + "description": "Snackbar while loading a CUE sheet file" + }, + "snackbarMetadataSaved": "Metadata saved successfully", + "@snackbarMetadataSaved": { + "description": "Snackbar after successfully saving track metadata" + }, + "snackbarFailedToEmbedLyrics": "Failed to embed lyrics", + "@snackbarFailedToEmbedLyrics": { + "description": "Snackbar when lyrics embedding fails" + }, + "snackbarFailedToWriteStorage": "Failed to write back to storage", + "@snackbarFailedToWriteStorage": { + "description": "Snackbar when writing metadata back to file fails" + }, + "snackbarError": "Error: {error}", + "@snackbarError": { + "description": "Generic error snackbar with error detail", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarNoActionDefined": "No action defined for this button", + "@snackbarNoActionDefined": { + "description": "Snackbar when an extension button has no action configured" + }, + "noTracksFoundForAlbum": "No tracks found for this album", + "@noTracksFoundForAlbum": { + "description": "Empty state message when an album has no tracks" + }, + "downloadLocationSubtitle": "Choose where to save your downloaded tracks", + "@downloadLocationSubtitle": { + "description": "Subtitle shown in the download location picker sheet" + }, + "storageModeAppFolder": "App Folder (Recommended)", + "@storageModeAppFolder": { + "description": "Storage mode option - app-managed folder" + }, + "storageModeAppFolderSubtitle": "Saves to Music/SpotiFLAC by default", + "@storageModeAppFolderSubtitle": { + "description": "Subtitle for app folder storage mode" + }, + "storageModeSaf": "Custom Folder (SAF)", + "@storageModeSaf": { + "description": "Storage mode option - Storage Access Framework" + }, + "storageModeSafSubtitle": "Pick any folder, including SD card", + "@storageModeSafSubtitle": { + "description": "Subtitle for SAF storage mode" + }, + "downloadFilenameDescription": "Use {artist}, {title}, {album}, {track}, {year}, {date}, {disc} as placeholders.", + "@downloadFilenameDescription": { + "description": "Description shown in filename format editor" + }, + "downloadFilenameInsertTag": "Tap to insert tag:", + "@downloadFilenameInsertTag": { + "description": "Label above filename tag chips" + }, + "downloadSeparateSinglesEnabled": "Singles and EPs saved in a separate folder", + "@downloadSeparateSinglesEnabled": { + "description": "Subtitle when separate singles folder is on" + }, + "downloadSeparateSinglesDisabled": "Singles and albums saved in the same folder", + "@downloadSeparateSinglesDisabled": { + "description": "Subtitle when separate singles folder is off" + }, + "downloadArtistNameFilters": "Artist Name Filters", + "@downloadArtistNameFilters": { + "description": "Setting title for artist folder filter options" + }, + "downloadCreatePlaylistSourceFolder": "Playlist Source Folder", + "@downloadCreatePlaylistSourceFolder": { + "description": "Setting to create a subfolder per playlist source" + }, + "downloadCreatePlaylistSourceFolderEnabled": "A subfolder is created for each playlist", + "@downloadCreatePlaylistSourceFolderEnabled": { + "description": "Subtitle when playlist folder is enabled" + }, + "downloadCreatePlaylistSourceFolderDisabled": "All tracks saved directly to download folder", + "@downloadCreatePlaylistSourceFolderDisabled": { + "description": "Subtitle when playlist folder is disabled" + }, + "downloadCreatePlaylistSourceFolderRedundant": "Handled by folder organization setting", + "@downloadCreatePlaylistSourceFolderRedundant": { + "description": "Subtitle when folder organization is already set to playlist" + }, + "downloadSongLinkRegion": "SongLink Region", + "@downloadSongLinkRegion": { + "description": "Setting for SongLink region used during fallback resolution" + }, + "downloadNetworkCompatibilityMode": "Network Compatibility Mode", + "@downloadNetworkCompatibilityMode": { + "description": "Setting for legacy TLS/network handling" + }, + "downloadNetworkCompatibilityModeEnabled": "Using legacy TLS settings for older networks", + "@downloadNetworkCompatibilityModeEnabled": { + "description": "Subtitle when network compatibility mode is on" + }, + "downloadNetworkCompatibilityModeDisabled": "Using standard network settings", + "@downloadNetworkCompatibilityModeDisabled": { + "description": "Subtitle when network compatibility mode is off" + }, + "downloadSelectServiceToEnable": "Select Tidal or Qobuz to enable this option", + "@downloadSelectServiceToEnable": { + "description": "Subtitle when quality picker is disabled due to extension service" + }, + "downloadSelectTidalQobuz": "Select Tidal or Qobuz to choose audio quality", + "@downloadSelectTidalQobuz": { + "description": "Info shown when a non-built-in service is selected" + }, + "downloadEmbedLyricsDisabled": "Enable metadata embedding first", + "@downloadEmbedLyricsDisabled": { + "description": "Subtitle when lyrics embedding is blocked by metadata toggle" + }, + "downloadNeteaseIncludeTranslation": "Netease: Include Translation", + "@downloadNeteaseIncludeTranslation": { + "description": "Setting to include translated lyrics from Netease" + }, + "downloadNeteaseIncludeTranslationEnabled": "Chinese translation lines included", + "@downloadNeteaseIncludeTranslationEnabled": { + "description": "Subtitle when Netease translation is on" + }, + "downloadNeteaseIncludeTranslationDisabled": "Original lyrics only", + "@downloadNeteaseIncludeTranslationDisabled": { + "description": "Subtitle when Netease translation is off" + }, + "downloadNeteaseIncludeRomanization": "Netease: Include Romanization", + "@downloadNeteaseIncludeRomanization": { + "description": "Setting to include romanized lyrics from Netease" + }, + "downloadNeteaseIncludeRomanizationEnabled": "Romanization lines included", + "@downloadNeteaseIncludeRomanizationEnabled": { + "description": "Subtitle when Netease romanization is on" + }, + "downloadNeteaseIncludeRomanizationDisabled": "No romanization", + "@downloadNeteaseIncludeRomanizationDisabled": { + "description": "Subtitle when Netease romanization is off" + }, + "downloadAppleQqMultiPerson": "Apple / QQ: Multi-Person Lyrics", + "@downloadAppleQqMultiPerson": { + "description": "Setting for word-by-word multi-person lyrics from Apple Music and QQ Music" + }, + "downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks", + "@downloadAppleQqMultiPersonEnabled": { + "description": "Subtitle when multi-person lyrics is on" + }, + "downloadAppleQqMultiPersonDisabled": "Standard lyrics without speaker labels", + "@downloadAppleQqMultiPersonDisabled": { + "description": "Subtitle when multi-person lyrics is off" + }, + "downloadMusixmatchLanguage": "Musixmatch Language", + "@downloadMusixmatchLanguage": { + "description": "Setting for Musixmatch lyrics translation language" + }, + "downloadMusixmatchLanguageAuto": "Auto (original language)", + "@downloadMusixmatchLanguageAuto": { + "description": "Subtitle when no language is set" + }, + "downloadFilterContributing": "Filter Contributing Artists", + "@downloadFilterContributing": { + "description": "Setting to strip contributing artists from Album Artist folder name" + }, + "downloadFilterContributingEnabled": "Contributing artists removed from Album Artist folder name", + "@downloadFilterContributingEnabled": { + "description": "Subtitle when contributing artist filter is on" + }, + "downloadFilterContributingDisabled": "Full Album Artist string used", + "@downloadFilterContributingDisabled": { + "description": "Subtitle when contributing artist filter is off" + }, + "downloadProvidersNoneEnabled": "No providers enabled", + "@downloadProvidersNoneEnabled": { + "description": "Shown when no lyrics providers are active" + }, + "downloadMusixmatchLanguageCode": "Language code", + "@downloadMusixmatchLanguageCode": { + "description": "Label for Musixmatch language input field" + }, + "downloadMusixmatchLanguageHint": "e.g. en, de, ja", + "@downloadMusixmatchLanguageHint": { + "description": "Placeholder for Musixmatch language input" + }, + "downloadMusixmatchLanguageDesc": "Enter a BCP-47 language code (e.g. en, de, ja) to request translated lyrics from Musixmatch.", + "@downloadMusixmatchLanguageDesc": { + "description": "Description in Musixmatch language picker" + }, + "downloadMusixmatchAuto": "Auto", + "@downloadMusixmatchAuto": { + "description": "Button to clear Musixmatch language (use auto)" + }, + "downloadNetworkAnySubtitle": "Use WiFi or mobile data", + "@downloadNetworkAnySubtitle": { + "description": "Subtitle for any-network option in picker" + }, + "downloadNetworkWifiOnlySubtitle": "Downloads pause when on mobile data", + "@downloadNetworkWifiOnlySubtitle": { + "description": "Subtitle for WiFi-only option in picker" + }, + "downloadSongLinkRegionDesc": "Region used when resolving track links via SongLink. Choose the country where your streaming services are available.", + "@downloadSongLinkRegionDesc": { + "description": "Description in SongLink region picker" + }, + "snackbarUnsupportedAudioFormat": "Unsupported audio format", + "@snackbarUnsupportedAudioFormat": { + "description": "Snackbar when the audio format is not supported for the requested operation" + }, + "cacheRefresh": "Refresh", + "@cacheRefresh": { + "description": "Tooltip for refresh button on cache management page" + }, + "dialogDownloadPlaylistsMessage": "Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?", + "@dialogDownloadPlaylistsMessage": { + "description": "Dialog message for bulk playlist download confirmation", + "placeholders": { + "trackCount": { + "type": "int" + }, + "playlistCount": { + "type": "int" + } + } + }, + "bulkDownloadPlaylistsButton": "Download {count} {count, plural, =1{playlist} other{playlists}}", + "@bulkDownloadPlaylistsButton": { + "description": "Button label for bulk downloading selected playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "bulkDownloadSelectPlaylists": "Select playlists to download", + "@bulkDownloadSelectPlaylists": { + "description": "Button label when no playlists are selected for download" + }, + "snackbarSelectedPlaylistsEmpty": "Selected playlists have no tracks", + "@snackbarSelectedPlaylistsEmpty": { + "description": "Snackbar when selected playlists contain no tracks" + }, + "playlistsCount": "{count, plural, =1{1 playlist} other{{count} playlists}}", + "@playlistsCount": { + "description": "Playlist count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFill": "Auto-fill from online", + "@editMetadataAutoFill": { + "description": "Section title for selective online metadata auto-fill in the edit metadata sheet" + }, + "editMetadataAutoFillDesc": "Select fields to fill automatically from online metadata", + "@editMetadataAutoFillDesc": { + "description": "Description for the auto-fill section" + }, + "editMetadataAutoFillFetch": "Fetch & Fill", + "@editMetadataAutoFillFetch": { + "description": "Button label to fetch online metadata and fill selected fields" + }, + "editMetadataAutoFillSearching": "Searching online...", + "@editMetadataAutoFillSearching": { + "description": "Snackbar shown while searching for online metadata" + }, + "editMetadataAutoFillNoResults": "No matching metadata found online", + "@editMetadataAutoFillNoResults": { + "description": "Snackbar when online metadata search returns no results" + }, + "editMetadataAutoFillDone": "Filled {count} {count, plural, =1{field} other{fields}} from online metadata", + "@editMetadataAutoFillDone": { + "description": "Snackbar confirming how many fields were auto-filled", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFillNoneSelected": "Select at least one field to auto-fill", + "@editMetadataAutoFillNoneSelected": { + "description": "Snackbar when user taps Fetch without selecting any fields" + }, + "editMetadataFieldTitle": "Title", + "@editMetadataFieldTitle": { + "description": "Chip label for title field in auto-fill selector" + }, + "editMetadataFieldArtist": "Artist", + "@editMetadataFieldArtist": { + "description": "Chip label for artist field in auto-fill selector" + }, + "editMetadataFieldAlbum": "Album", + "@editMetadataFieldAlbum": { + "description": "Chip label for album field in auto-fill selector" + }, + "editMetadataFieldAlbumArtist": "Album Artist", + "@editMetadataFieldAlbumArtist": { + "description": "Chip label for album artist field in auto-fill selector" + }, + "editMetadataFieldDate": "Date", + "@editMetadataFieldDate": { + "description": "Chip label for date field in auto-fill selector" + }, + "editMetadataFieldTrackNum": "Track #", + "@editMetadataFieldTrackNum": { + "description": "Chip label for track number field in auto-fill selector" + }, + "editMetadataFieldDiscNum": "Disc #", + "@editMetadataFieldDiscNum": { + "description": "Chip label for disc number field in auto-fill selector" + }, + "editMetadataFieldGenre": "Genre", + "@editMetadataFieldGenre": { + "description": "Chip label for genre field in auto-fill selector" + }, + "editMetadataFieldIsrc": "ISRC", + "@editMetadataFieldIsrc": { + "description": "Chip label for ISRC field in auto-fill selector" + }, + "editMetadataFieldLabel": "Label", + "@editMetadataFieldLabel": { + "description": "Chip label for label field in auto-fill selector" + }, + "editMetadataFieldCopyright": "Copyright", + "@editMetadataFieldCopyright": { + "description": "Chip label for copyright field in auto-fill selector" + }, + "editMetadataFieldCover": "Cover Art", + "@editMetadataFieldCover": { + "description": "Chip label for cover art field in auto-fill selector" + }, + "editMetadataSelectAll": "All", + "@editMetadataSelectAll": { + "description": "Button to select all fields for auto-fill" + }, + "editMetadataSelectEmpty": "Empty only", + "@editMetadataSelectEmpty": { + "description": "Button to select only fields that are currently empty" + }, + "queueDownloadingCount": "Downloading ({count})", + "@queueDownloadingCount": { + "description": "Header for active downloads section with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueDownloadedHeader": "Downloaded", + "@queueDownloadedHeader": { + "description": "Header label for downloaded items section in library" + }, + "queueFilteringIndicator": "Filtering...", + "@queueFilteringIndicator": { + "description": "Shown while filter results are being computed" + }, + "queueTrackCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@queueTrackCount": { + "description": "Track count label with plural support", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueAlbumCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@queueAlbumCount": { + "description": "Album count label with plural support", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueEmptyAlbums": "No album downloads", + "@queueEmptyAlbums": { + "description": "Empty state title when no album downloads exist" + }, + "queueEmptyAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@queueEmptyAlbumsSubtitle": { + "description": "Empty state subtitle for album downloads" + }, + "queueEmptySingles": "No single downloads", + "@queueEmptySingles": { + "description": "Empty state title when no single track downloads exist" + }, + "queueEmptySinglesSubtitle": "Single track downloads will appear here", + "@queueEmptySinglesSubtitle": { + "description": "Empty state subtitle for single track downloads" + }, + "queueEmptyHistory": "No download history", + "@queueEmptyHistory": { + "description": "Empty state title when download history is empty" + }, + "queueEmptyHistorySubtitle": "Downloaded tracks will appear here", + "@queueEmptyHistorySubtitle": { + "description": "Empty state subtitle for download history" + }, + "selectionAllPlaylistsSelected": "All playlists selected", + "@selectionAllPlaylistsSelected": { + "description": "Shown when all playlists are selected in selection mode" + }, + "selectionTapPlaylistsToSelect": "Tap playlists to select", + "@selectionTapPlaylistsToSelect": { + "description": "Hint shown in playlist selection mode" + }, + "selectionSelectPlaylistsToDelete": "Select playlists to delete", + "@selectionSelectPlaylistsToDelete": { + "description": "Hint shown when no playlists are selected for deletion" + }, + "audioAnalysisTitle": "Audio Quality Analysis", + "@audioAnalysisTitle": { + "description": "Title for audio analysis section" + }, + "audioAnalysisDescription": "Verify lossless quality with spectrum analysis", + "@audioAnalysisDescription": { + "description": "Description for audio analysis tap-to-analyze prompt" + }, + "audioAnalysisAnalyzing": "Analyzing audio...", + "@audioAnalysisAnalyzing": { + "description": "Loading text while analyzing audio" + }, + "audioAnalysisSampleRate": "Sample Rate", + "@audioAnalysisSampleRate": { + "description": "Sample rate metric label" + }, + "audioAnalysisBitDepth": "Bit Depth", + "@audioAnalysisBitDepth": { + "description": "Bit depth metric label" + }, + "audioAnalysisChannels": "Channels", + "@audioAnalysisChannels": { + "description": "Channels metric label" + }, + "audioAnalysisDuration": "Duration", + "@audioAnalysisDuration": { + "description": "Duration metric label" + }, + "audioAnalysisNyquist": "Nyquist", + "@audioAnalysisNyquist": { + "description": "Nyquist frequency metric label" + }, + "audioAnalysisFileSize": "Size", + "@audioAnalysisFileSize": { + "description": "File size metric label" + }, + "audioAnalysisDynamicRange": "Dynamic Range", + "@audioAnalysisDynamicRange": { + "description": "Dynamic range metric label" + }, + "audioAnalysisPeak": "Peak", + "@audioAnalysisPeak": { + "description": "Peak amplitude metric label" + }, + "audioAnalysisRms": "RMS", + "@audioAnalysisRms": { + "description": "RMS level metric label" + }, + "audioAnalysisSamples": "Samples", + "@audioAnalysisSamples": { + "description": "Total samples metric label" + }, + "extensionsSearchWith": "Search with {providerName}", + "@extensionsSearchWith": { + "description": "Extensions page - subtitle for built-in search provider option", + "placeholders": { + "providerName": { + "type": "String" + } + } + }, + "extensionsHomeFeedProvider": "Home Feed Provider", + "@extensionsHomeFeedProvider": { + "description": "Extensions page - label for home feed provider selector" + }, + "extensionsHomeFeedDescription": "Choose which extension provides the home feed on the main screen", + "@extensionsHomeFeedDescription": { + "description": "Extensions page - description for home feed provider picker" + }, + "extensionsHomeFeedAuto": "Auto", + "@extensionsHomeFeedAuto": { + "description": "Label for auto-selected search provider" + }, + "extensionsHomeFeedAutoSubtitle": "Automatically select the best available", + "@extensionsHomeFeedAutoSubtitle": { + "description": "Extensions page - subtitle for auto home feed option" + }, + "extensionsHomeFeedUse": "Use {extensionName} home feed", + "@extensionsHomeFeedUse": { + "description": "Extensions page - subtitle for a specific extension home feed option", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "extensionsNoHomeFeedExtensions": "No extensions with home feed", + "@extensionsNoHomeFeedExtensions": { + "description": "Extensions page - shown when no installed extension has home feed" + }, + "sortAlphaAsc": "A-Z", + "@sortAlphaAsc": { + "description": "Sort option - alphabetical ascending" + }, + "sortAlphaDesc": "Z-A", + "@sortAlphaDesc": { + "description": "Sort option - alphabetical descending" + }, + "cancelDownloadTitle": "Cancel download?", + "@cancelDownloadTitle": { + "description": "Dialog title when confirming cancellation of an active download" + }, + "cancelDownloadContent": "This will cancel the active download for \"{trackName}\".", + "@cancelDownloadContent": { + "description": "Dialog body when confirming cancellation of an active download", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "cancelDownloadKeep": "Keep", + "@cancelDownloadKeep": { + "description": "Dialog button - keep the active download (do not cancel)" + }, + "metadataSaveFailedFfmpeg": "Failed to save metadata via FFmpeg", + "@metadataSaveFailedFfmpeg": { + "description": "Snackbar error when FFmpeg fails to write metadata" + }, + "metadataSaveFailedStorage": "Failed to write metadata back to storage", + "@metadataSaveFailedStorage": { + "description": "Snackbar error when writing metadata file back to storage fails" + }, + "snackbarFolderPickerFailed": "Failed to open folder picker: {error}", + "@snackbarFolderPickerFailed": { + "description": "Snackbar shown when folder picker fails to open", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "errorLoadAlbum": "Failed to load album", + "@errorLoadAlbum": { + "description": "Error state shown when album fails to load" + }, + "errorLoadPlaylist": "Failed to load playlist", + "@errorLoadPlaylist": { + "description": "Error state shown when playlist fails to load" + }, + "errorLoadArtist": "Failed to load artist", + "@errorLoadArtist": { + "description": "Error state shown when artist fails to load" + }, + "notifChannelDownloadName": "Download Progress", + "@notifChannelDownloadName": { + "description": "Android notification channel name for download progress" + }, + "notifChannelDownloadDesc": "Shows download progress for tracks", + "@notifChannelDownloadDesc": { + "description": "Android notification channel description for download progress" + }, + "notifChannelLibraryScanName": "Library Scan", + "@notifChannelLibraryScanName": { + "description": "Android notification channel name for library scan" + }, + "notifChannelLibraryScanDesc": "Shows local library scan progress", + "@notifChannelLibraryScanDesc": { + "description": "Android notification channel description for library scan" + }, + "notifDownloadingTrack": "Downloading {trackName}", + "@notifDownloadingTrack": { + "description": "Notification title while downloading a track", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "notifFinalizingTrack": "Finalizing {trackName}", + "@notifFinalizingTrack": { + "description": "Notification title while finalizing (embedding metadata) a track", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "notifEmbeddingMetadata": "Embedding metadata...", + "@notifEmbeddingMetadata": { + "description": "Notification body while embedding metadata into a downloaded track" + }, + "notifAlreadyInLibraryCount": "Already in Library ({completed}/{total})", + "@notifAlreadyInLibraryCount": { + "description": "Notification title when track is already in library, with count", + "placeholders": { + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "notifAlreadyInLibrary": "Already in Library", + "@notifAlreadyInLibrary": { + "description": "Notification title when track is already in library" + }, + "notifDownloadCompleteCount": "Download Complete ({completed}/{total})", + "@notifDownloadCompleteCount": { + "description": "Notification title when download is complete, with count", + "placeholders": { + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "notifDownloadComplete": "Download Complete", + "@notifDownloadComplete": { + "description": "Notification title when a single download is complete" + }, + "notifDownloadsFinished": "Downloads Finished ({completed} done, {failed} failed)", + "@notifDownloadsFinished": { + "description": "Notification title when queue finishes with some failures", + "placeholders": { + "completed": { + "type": "int" + }, + "failed": { + "type": "int" + } + } + }, + "notifAllDownloadsComplete": "All Downloads Complete", + "@notifAllDownloadsComplete": { + "description": "Notification title when all downloads finish successfully" + }, + "notifTracksDownloadedSuccess": "{count} tracks downloaded successfully", + "@notifTracksDownloadedSuccess": { + "description": "Notification body for queue complete - how many tracks were downloaded", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifScanningLibrary": "Scanning local library", + "@notifScanningLibrary": { + "description": "Notification title while scanning local library" + }, + "notifLibraryScanProgressWithTotal": "{scanned}/{total} files • {percentage}%", + "@notifLibraryScanProgressWithTotal": { + "description": "Notification body for library scan progress when total is known", + "placeholders": { + "scanned": { + "type": "int" + }, + "total": { + "type": "int" + }, + "percentage": { + "type": "int" + } + } + }, + "notifLibraryScanProgressNoTotal": "{scanned} files scanned • {percentage}%", + "@notifLibraryScanProgressNoTotal": { + "description": "Notification body for library scan progress when total is unknown", + "placeholders": { + "scanned": { + "type": "int" + }, + "percentage": { + "type": "int" + } + } + }, + "notifLibraryScanComplete": "Library scan complete", + "@notifLibraryScanComplete": { + "description": "Notification title when library scan finishes" + }, + "notifLibraryScanCompleteBody": "{count} tracks indexed", + "@notifLibraryScanCompleteBody": { + "description": "Notification body for library scan complete - number of indexed tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanExcluded": "{count} excluded", + "@notifLibraryScanExcluded": { + "description": "Library scan complete suffix - excluded track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanErrors": "{count} errors", + "@notifLibraryScanErrors": { + "description": "Library scan complete suffix - error count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanFailed": "Library scan failed", + "@notifLibraryScanFailed": { + "description": "Notification title when library scan fails" + }, + "notifLibraryScanCancelled": "Library scan cancelled", + "@notifLibraryScanCancelled": { + "description": "Notification title when library scan is cancelled by the user" + }, + "notifLibraryScanStopped": "Scan stopped before completion.", + "@notifLibraryScanStopped": { + "description": "Notification body when library scan is cancelled" + }, + "notifDownloadingUpdate": "Downloading SpotiFLAC v{version}", + "@notifDownloadingUpdate": { + "description": "Notification title while downloading an app update", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "notifUpdateProgress": "{received} / {total} MB • {percentage}%", + "@notifUpdateProgress": { + "description": "Notification body showing update download progress", + "placeholders": { + "received": { + "type": "String" + }, + "total": { + "type": "String" + }, + "percentage": { + "type": "int" + } + } + }, + "notifUpdateReady": "Update Ready", + "@notifUpdateReady": { + "description": "Notification title when app update download is complete" + }, + "notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.", + "@notifUpdateReadyBody": { + "description": "Notification body when app update is ready to install", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "notifUpdateFailed": "Update Failed", + "@notifUpdateFailed": { + "description": "Notification title when app update download fails" + }, + "notifUpdateFailedBody": "Could not download update. Try again later.", + "@notifUpdateFailedBody": { + "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } diff --git a/lib/l10n/arb/app_pt_PT.arb b/lib/l10n/arb/app_pt_PT.arb index 3cf494eb..5b7a11fc 100644 --- a/lib/l10n/arb/app_pt_PT.arb +++ b/lib/l10n/arb/app_pt_PT.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index c3d650fa..54b47b1a 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_tr.arb b/lib/l10n/arb/app_tr.arb index e0aa8df7..9ffbe3b0 100644 --- a/lib/l10n/arb/app_tr.arb +++ b/lib/l10n/arb/app_tr.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_uk.arb b/lib/l10n/arb/app_uk.arb index 62c47453..d9f2ed50 100644 --- a/lib/l10n/arb/app_uk.arb +++ b/lib/l10n/arb/app_uk.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Не вдалося завантажити оновлення. Спробуйте пізніше.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index 307f156b..3127c84e 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -1728,5 +1728,2858 @@ "type": "int" } } + }, + "navLibrary": "Library", + "@navLibrary": { + "description": "Bottom navigation - Library tab" + }, + "historySearchHint": "Search history...", + "@historySearchHint": { + "description": "Search bar placeholder in history" + }, + "downloadSingleFilenameFormat": "Single Filename Format", + "@downloadSingleFilenameFormat": { + "description": "Setting for output filename pattern for singles/EPs" + }, + "downloadSingleFilenameFormatDescription": "Filename pattern for singles and EPs. Uses the same tags as the album format.", + "@downloadSingleFilenameFormatDescription": { + "description": "Subtitle description for single filename format setting" + }, + "optionsDefaultSearchTab": "Default Search Tab", + "@optionsDefaultSearchTab": { + "description": "Title for the preferred default search tab setting" + }, + "optionsDefaultSearchTabSubtitle": "Choose which tab opens first for new search results.", + "@optionsDefaultSearchTabSubtitle": { + "description": "Subtitle for the preferred default search tab setting" + }, + "optionsReplayGain": "ReplayGain", + "@optionsReplayGain": { + "description": "Title for ReplayGain setting toggle" + }, + "optionsReplayGainSubtitleOn": "Scan loudness and embed ReplayGain tags (EBU R128)", + "@optionsReplayGainSubtitleOn": { + "description": "Subtitle when ReplayGain is enabled" + }, + "optionsReplayGainSubtitleOff": "Disabled: no loudness normalization tags", + "@optionsReplayGainSubtitleOff": { + "description": "Subtitle when ReplayGain is disabled" + }, + "optionsArtistTagMode": "Artist Tag Mode", + "@optionsArtistTagMode": { + "description": "Setting title for how artist metadata is written into files" + }, + "optionsArtistTagModeDescription": "Choose how multiple artists are written into embedded tags.", + "@optionsArtistTagModeDescription": { + "description": "Bottom-sheet description for artist tag mode setting" + }, + "optionsArtistTagModeJoined": "Single joined value", + "@optionsArtistTagModeJoined": { + "description": "Artist tag mode option that joins multiple artists into one value" + }, + "optionsArtistTagModeJoinedSubtitle": "Write one ARTIST value like \"Artist A, Artist B\" for maximum player compatibility.", + "@optionsArtistTagModeJoinedSubtitle": { + "description": "Subtitle for joined artist tag mode" + }, + "optionsArtistTagModeSplitVorbis": "Split tags for FLAC/Opus", + "@optionsArtistTagModeSplitVorbis": { + "description": "Artist tag mode option that writes repeated ARTIST tags for Vorbis formats" + }, + "optionsArtistTagModeSplitVorbisSubtitle": "Write one artist tag per artist for FLAC and Opus; MP3 and M4A stay joined.", + "@optionsArtistTagModeSplitVorbisSubtitle": { + "description": "Subtitle for split Vorbis artist tag mode" + }, + "optionsSpotifyDeprecationWarning": "Spotify search will be deprecated on March 3, 2026 due to Spotify API changes. Please switch to Deezer.", + "@optionsSpotifyDeprecationWarning": { + "description": "Warning about Spotify API deprecation" + }, + "aboutTranslators": "Translators", + "@aboutTranslators": { + "description": "Section for translators" + }, + "aboutTelegramChannel": "Telegram Channel", + "@aboutTelegramChannel": { + "description": "Link to Telegram channel" + }, + "aboutTelegramChannelSubtitle": "Announcements and updates", + "@aboutTelegramChannelSubtitle": { + "description": "Subtitle for Telegram channel" + }, + "aboutTelegramChat": "Telegram Community", + "@aboutTelegramChat": { + "description": "Link to Telegram chat group" + }, + "aboutTelegramChatSubtitle": "Chat with other users", + "@aboutTelegramChatSubtitle": { + "description": "Subtitle for Telegram chat" + }, + "aboutSocial": "Social", + "@aboutSocial": { + "description": "Section for social links" + }, + "aboutSjdonadoDesc": "Creator of I Don't Have Spotify (IDHS). The fallback link resolver that saves the day!", + "@aboutSjdonadoDesc": { + "description": "Credit description for sjdonado" + }, + "aboutSpotiSaver": "SpotiSaver", + "@aboutSpotiSaver": { + "description": "Name of SpotiSaver API service - DO NOT TRANSLATE" + }, + "aboutSpotiSaverDesc": "Tidal Hi-Res FLAC streaming endpoints. A key piece of the lossless puzzle!", + "@aboutSpotiSaverDesc": { + "description": "Credit for SpotiSaver API" + }, + "artistPopular": "Popular", + "@artistPopular": { + "description": "Section header for popular/top tracks" + }, + "artistMonthlyListeners": "{count} monthly listeners", + "@artistMonthlyListeners": { + "description": "Monthly listener count display", + "placeholders": { + "count": { + "type": "String", + "description": "Formatted listener count" + } + } + }, + "setupIcloudNotSupported": "iCloud Drive is not supported. Please use the app Documents folder.", + "@setupIcloudNotSupported": { + "description": "Error when user selects iCloud Drive on iOS" + }, + "dialogDownload": "Download", + "@dialogDownload": { + "description": "Confirm button in Download All dialog" + }, + "csvImportTracks": "{count} tracks from CSV", + "@csvImportTracks": { + "description": "Label shown in quality picker for CSV import", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAlreadyInLibrary": "\"{trackName}\" already exists in your library", + "@snackbarAlreadyInLibrary": { + "description": "Snackbar - track already exists in local library", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "errorUrlNotRecognized": "Link not recognized", + "@errorUrlNotRecognized": { + "description": "Error title - URL not handled by any extension or service" + }, + "errorUrlNotRecognizedMessage": "This link is not supported. Make sure the URL is correct and a compatible extension is installed.", + "@errorUrlNotRecognizedMessage": { + "description": "Error message - URL not recognized explanation" + }, + "errorUrlFetchFailed": "Failed to load content from this link. Please try again.", + "@errorUrlFetchFailed": { + "description": "Error message - generic URL fetch failure" + }, + "searchSortTitle": "Sort Results", + "@searchSortTitle": { + "description": "Bottom sheet title for search sort options" + }, + "searchSortDefault": "Default", + "@searchSortDefault": { + "description": "Sort option - default API order" + }, + "searchSortTitleAZ": "Title (A-Z)", + "@searchSortTitleAZ": { + "description": "Sort option - title ascending" + }, + "searchSortTitleZA": "Title (Z-A)", + "@searchSortTitleZA": { + "description": "Sort option - title descending" + }, + "searchSortArtistAZ": "Artist (A-Z)", + "@searchSortArtistAZ": { + "description": "Sort option - artist ascending" + }, + "searchSortArtistZA": "Artist (Z-A)", + "@searchSortArtistZA": { + "description": "Sort option - artist descending" + }, + "searchSortDurationShort": "Duration (Shortest)", + "@searchSortDurationShort": { + "description": "Sort option - shortest duration first" + }, + "searchSortDurationLong": "Duration (Longest)", + "@searchSortDurationLong": { + "description": "Sort option - longest duration first" + }, + "searchSortDateOldest": "Release Date (Oldest)", + "@searchSortDateOldest": { + "description": "Sort option - oldest release first" + }, + "searchSortDateNewest": "Release Date (Newest)", + "@searchSortDateNewest": { + "description": "Sort option - newest release first" + }, + "filenameShowAdvancedTags": "Show advanced tags", + "@filenameShowAdvancedTags": { + "description": "Toggle label for showing advanced filename tags" + }, + "filenameShowAdvancedTagsDescription": "Enable formatted tags for track padding and date patterns", + "@filenameShowAdvancedTagsDescription": { + "description": "Description for advanced filename tag toggle" + }, + "folderOrganizationByPlaylist": "By Playlist", + "@folderOrganizationByPlaylist": { + "description": "Folder option - playlist folders" + }, + "folderOrganizationByPlaylistSubtitle": "Separate folder for each playlist", + "@folderOrganizationByPlaylistSubtitle": { + "description": "Subtitle for playlist folder option" + }, + "providerPriorityFallbackExtensionsTitle": "Extension Fallback", + "@providerPriorityFallbackExtensionsTitle": { + "description": "Section title for choosing which download extensions can be used as fallback providers" + }, + "providerPriorityFallbackExtensionsDescription": "Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.", + "@providerPriorityFallbackExtensionsDescription": { + "description": "Section description for extension fallback selection" + }, + "providerPriorityFallbackExtensionsHint": "Only enabled extensions with download-provider capability are listed here.", + "@providerPriorityFallbackExtensionsHint": { + "description": "Hint below the extension fallback selection list" + }, + "sectionLyrics": "Lyrics", + "@sectionLyrics": { + "description": "Settings section header" + }, + "lyricsMode": "Lyrics Mode", + "@lyricsMode": { + "description": "Setting - how to save lyrics" + }, + "lyricsModeDescription": "Choose how lyrics are saved with your downloads", + "@lyricsModeDescription": { + "description": "Lyrics mode picker description" + }, + "lyricsModeEmbed": "Embed in file", + "@lyricsModeEmbed": { + "description": "Lyrics mode option - embed in audio file" + }, + "lyricsModeEmbedSubtitle": "Lyrics stored inside FLAC metadata", + "@lyricsModeEmbedSubtitle": { + "description": "Subtitle for embed option" + }, + "lyricsModeExternal": "External .lrc file", + "@lyricsModeExternal": { + "description": "Lyrics mode option - separate LRC file" + }, + "lyricsModeExternalSubtitle": "Separate .lrc file for players like Samsung Music", + "@lyricsModeExternalSubtitle": { + "description": "Subtitle for external option" + }, + "lyricsModeBoth": "Both", + "@lyricsModeBoth": { + "description": "Lyrics mode option - embed and external" + }, + "lyricsModeBothSubtitle": "Embed and save .lrc file", + "@lyricsModeBothSubtitle": { + "description": "Subtitle for both option" + }, + "trackGenre": "Genre", + "@trackGenre": { + "description": "Metadata label - music genre" + }, + "trackLabel": "Label", + "@trackLabel": { + "description": "Metadata label - record label" + }, + "trackCopyright": "Copyright", + "@trackCopyright": { + "description": "Metadata label - copyright information" + }, + "trackLyricsNotInFile": "No lyrics found in this file", + "@trackLyricsNotInFile": { + "description": "Message when no embedded lyrics in audio file" + }, + "trackFetchOnlineLyrics": "Fetch from Online", + "@trackFetchOnlineLyrics": { + "description": "Action - fetch lyrics from online providers" + }, + "trackEmbedLyrics": "Embed Lyrics", + "@trackEmbedLyrics": { + "description": "Action - embed lyrics into audio file" + }, + "trackLyricsEmbedded": "Lyrics embedded successfully", + "@trackLyricsEmbedded": { + "description": "Snackbar - lyrics saved to file" + }, + "trackInstrumental": "Instrumental track", + "@trackInstrumental": { + "description": "Message when track is instrumental (no lyrics)" + }, + "storeAddRepoTitle": "Add Extension Repository", + "@storeAddRepoTitle": { + "description": "Store setup screen - heading when no repo is configured" + }, + "storeAddRepoDescription": "Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.", + "@storeAddRepoDescription": { + "description": "Store setup screen - explanatory text" + }, + "storeRepoUrlLabel": "Repository URL", + "@storeRepoUrlLabel": { + "description": "Label for the repository URL input field" + }, + "storeRepoUrlHint": "https://github.com/user/repo", + "@storeRepoUrlHint": { + "description": "Hint/placeholder for the repository URL input field" + }, + "storeRepoUrlHelper": "e.g. https://github.com/user/extensions-repo", + "@storeRepoUrlHelper": { + "description": "Helper text below the repository URL input field" + }, + "storeAddRepoButton": "Add Repository", + "@storeAddRepoButton": { + "description": "Button to submit a new repository URL" + }, + "storeChangeRepoTooltip": "Change repository", + "@storeChangeRepoTooltip": { + "description": "Tooltip for the change-repository icon button in the app bar" + }, + "storeRepoDialogTitle": "Extension Repository", + "@storeRepoDialogTitle": { + "description": "Title of the change/remove repository dialog" + }, + "storeRepoDialogCurrent": "Current repository:", + "@storeRepoDialogCurrent": { + "description": "Label shown above the current repository URL in the dialog" + }, + "storeNewRepoUrlLabel": "New Repository URL", + "@storeNewRepoUrlLabel": { + "description": "Label for the new repository URL field inside the dialog" + }, + "storeLoadError": "Failed to load repository", + "@storeLoadError": { + "description": "Error heading when the store cannot be loaded" + }, + "storeEmptyNoExtensions": "No extensions available", + "@storeEmptyNoExtensions": { + "description": "Message when store has no extensions" + }, + "storeEmptyNoResults": "No extensions found", + "@storeEmptyNoResults": { + "description": "Message when search/filter returns no results" + }, + "extensionsFallbackTitle": "Fallback Extensions", + "@extensionsFallbackTitle": { + "description": "Setting and page title for choosing which download extensions can be used during fallback" + }, + "extensionsFallbackSubtitle": "Choose which installed download extensions can be used as fallback", + "@extensionsFallbackSubtitle": { + "description": "Subtitle for download fallback extensions menu" + }, + "downloadLossy320": "Lossy 320kbps", + "@downloadLossy320": { + "description": "Quality option label for Tidal lossy 320kbps" + }, + "downloadLossyFormat": "Lossy Format", + "@downloadLossyFormat": { + "description": "Setting title to pick output format for Tidal lossy downloads" + }, + "downloadLossy320Format": "Lossy 320kbps Format", + "@downloadLossy320Format": { + "description": "Title of the Tidal lossy format picker bottom sheet" + }, + "downloadLossy320FormatDesc": "Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.", + "@downloadLossy320FormatDesc": { + "description": "Description in the Tidal lossy format picker" + }, + "downloadLossyMp3": "MP3 320kbps", + "@downloadLossyMp3": { + "description": "Tidal lossy format option - MP3 320kbps" + }, + "downloadLossyMp3Subtitle": "Best compatibility, ~10MB per track", + "@downloadLossyMp3Subtitle": { + "description": "Subtitle for MP3 320kbps Tidal lossy option" + }, + "downloadLossyOpus256": "Opus 256kbps", + "@downloadLossyOpus256": { + "description": "Tidal lossy format option - Opus 256kbps" + }, + "downloadLossyOpus256Subtitle": "Best quality Opus, ~8MB per track", + "@downloadLossyOpus256Subtitle": { + "description": "Subtitle for Opus 256kbps Tidal lossy option" + }, + "downloadLossyOpus128": "Opus 128kbps", + "@downloadLossyOpus128": { + "description": "Tidal lossy format option - Opus 128kbps" + }, + "downloadLossyOpus128Subtitle": "Smallest size, ~4MB per track", + "@downloadLossyOpus128Subtitle": { + "description": "Subtitle for Opus 128kbps Tidal lossy option" + }, + "downloadUseAlbumArtistForFolders": "Use Album Artist for folders", + "@downloadUseAlbumArtistForFolders": { + "description": "Setting - choose whether artist folders use Album Artist or Track Artist" + }, + "downloadUsePrimaryArtistOnly": "Primary artist only for folders", + "@downloadUsePrimaryArtistOnly": { + "description": "Setting - strip featured artists from folder name" + }, + "downloadUsePrimaryArtistOnlyEnabled": "Featured artists removed from folder name (e.g. Justin Bieber, Quavo → Justin Bieber)", + "@downloadUsePrimaryArtistOnlyEnabled": { + "description": "Subtitle when primary artist only is enabled" + }, + "downloadUsePrimaryArtistOnlyDisabled": "Full artist string used for folder name", + "@downloadUsePrimaryArtistOnlyDisabled": { + "description": "Subtitle when primary artist only is disabled" + }, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": { + "description": "Setting toggle for auto-export" + }, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": { + "description": "Subtitle for auto-export setting" + }, + "settingsDownloadNetwork": "Download Network", + "@settingsDownloadNetwork": { + "description": "Setting for network type preference" + }, + "settingsDownloadNetworkAny": "WiFi + Mobile Data", + "@settingsDownloadNetworkAny": { + "description": "Network option - use any connection" + }, + "settingsDownloadNetworkWifiOnly": "WiFi Only", + "@settingsDownloadNetworkWifiOnly": { + "description": "Network option - only use WiFi" + }, + "settingsDownloadNetworkSubtitle": "Choose which network to use for downloads. When set to WiFi Only, downloads will pause on mobile data.", + "@settingsDownloadNetworkSubtitle": { + "description": "Subtitle explaining network preference" + }, + "albumFolderArtistAlbumSingles": "Artist / Album + Singles", + "@albumFolderArtistAlbumSingles": { + "description": "Album folder option with singles inside artist" + }, + "albumFolderArtistAlbumSinglesSubtitle": "Artist/Album/ and Artist/Singles/", + "@albumFolderArtistAlbumSinglesSubtitle": { + "description": "Folder structure example" + }, + "albumFolderArtistAlbumFlat": "Artist / Album (Singles flat)", + "@albumFolderArtistAlbumFlat": { + "description": "Album folder option with singles directly in artist folder" + }, + "albumFolderArtistAlbumFlatSubtitle": "Artist/Album/ and Artist/song.flac", + "@albumFolderArtistAlbumFlatSubtitle": { + "description": "Folder structure example for flat singles" + }, + "downloadedAlbumDiscHeader": "Disc {discNumber}", + "@downloadedAlbumDiscHeader": { + "description": "Header for disc separator in multi-disc albums", + "placeholders": { + "discNumber": { + "type": "int", + "example": "1" + } + } + }, + "recentTypeArtist": "Artist", + "@recentTypeArtist": { + "description": "Recent access item type - artist" + }, + "recentTypeAlbum": "Album", + "@recentTypeAlbum": { + "description": "Recent access item type - album" + }, + "recentTypeSong": "Song", + "@recentTypeSong": { + "description": "Recent access item type - song/track" + }, + "recentTypePlaylist": "Playlist", + "@recentTypePlaylist": { + "description": "Recent access item type - playlist" + }, + "recentEmpty": "No recent items yet", + "@recentEmpty": { + "description": "Empty state text for recent access list" + }, + "recentShowAllDownloads": "Show All Downloads", + "@recentShowAllDownloads": { + "description": "Button label to unhide hidden downloads in recent access" + }, + "recentPlaylistInfo": "Playlist: {name}", + "@recentPlaylistInfo": { + "description": "Snackbar message when tapping playlist in recent access", + "placeholders": { + "name": { + "type": "String", + "description": "Playlist name" + } + } + }, + "discographyDownload": "Download Discography", + "@discographyDownload": { + "description": "Button - download artist discography" + }, + "discographyDownloadAll": "Download All", + "@discographyDownloadAll": { + "description": "Option - download entire discography" + }, + "discographyDownloadAllSubtitle": "{count} tracks from {albumCount} releases", + "@discographyDownloadAllSubtitle": { + "description": "Subtitle showing total tracks and albums", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographyAlbumsOnly": "Albums Only", + "@discographyAlbumsOnly": { + "description": "Option - download only albums" + }, + "discographyAlbumsOnlySubtitle": "{count} tracks from {albumCount} albums", + "@discographyAlbumsOnlySubtitle": { + "description": "Subtitle showing album tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySinglesOnly": "Singles & EPs Only", + "@discographySinglesOnly": { + "description": "Option - download only singles" + }, + "discographySinglesOnlySubtitle": "{count} tracks from {albumCount} singles", + "@discographySinglesOnlySubtitle": { + "description": "Subtitle showing singles tracks count", + "placeholders": { + "count": { + "type": "int" + }, + "albumCount": { + "type": "int" + } + } + }, + "discographySelectAlbums": "Select Albums...", + "@discographySelectAlbums": { + "description": "Option - manually select albums to download" + }, + "discographySelectAlbumsSubtitle": "Choose specific albums or singles", + "@discographySelectAlbumsSubtitle": { + "description": "Subtitle for select albums option" + }, + "discographyFetchingTracks": "Fetching tracks...", + "@discographyFetchingTracks": { + "description": "Progress - fetching album tracks" + }, + "discographyFetchingAlbum": "Fetching {current} of {total}...", + "@discographyFetchingAlbum": { + "description": "Progress - fetching specific album", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "discographySelectedCount": "{count} selected", + "@discographySelectedCount": { + "description": "Selection count badge", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographyDownloadSelected": "Download Selected", + "@discographyDownloadSelected": { + "description": "Button - download selected albums" + }, + "discographyAddedToQueue": "Added {count} tracks to queue", + "@discographyAddedToQueue": { + "description": "Snackbar - tracks added from discography", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "discographySkippedDownloaded": "{added} added, {skipped} already downloaded", + "@discographySkippedDownloaded": { + "description": "Snackbar - with skipped tracks count", + "placeholders": { + "added": { + "type": "int" + }, + "skipped": { + "type": "int" + } + } + }, + "discographyNoAlbums": "No albums available", + "@discographyNoAlbums": { + "description": "Error - no albums found for artist" + }, + "discographyFailedToFetch": "Failed to fetch some albums", + "@discographyFailedToFetch": { + "description": "Error - some albums failed to load" + }, + "sectionStorageAccess": "Storage Access", + "@sectionStorageAccess": { + "description": "Section header for storage access settings" + }, + "allFilesAccess": "All Files Access", + "@allFilesAccess": { + "description": "Toggle for MANAGE_EXTERNAL_STORAGE permission" + }, + "allFilesAccessEnabledSubtitle": "Can write to any folder", + "@allFilesAccessEnabledSubtitle": { + "description": "Subtitle when all files access is enabled" + }, + "allFilesAccessDisabledSubtitle": "Limited to media folders only", + "@allFilesAccessDisabledSubtitle": { + "description": "Subtitle when all files access is disabled" + }, + "allFilesAccessDescription": "Enable this if you encounter write errors when saving to custom folders. Android 13+ restricts access to certain directories by default.", + "@allFilesAccessDescription": { + "description": "Description explaining when to enable all files access" + }, + "allFilesAccessDeniedMessage": "Permission was denied. Please enable 'All files access' manually in system settings.", + "@allFilesAccessDeniedMessage": { + "description": "Message when permission is permanently denied" + }, + "allFilesAccessDisabledMessage": "All Files Access disabled. The app will use limited storage access.", + "@allFilesAccessDisabledMessage": { + "description": "Snackbar message when user disables all files access" + }, + "settingsLocalLibrary": "Local Library", + "@settingsLocalLibrary": { + "description": "Settings menu item - local library" + }, + "settingsLocalLibrarySubtitle": "Scan music & detect duplicates", + "@settingsLocalLibrarySubtitle": { + "description": "Subtitle for local library settings" + }, + "settingsCache": "Storage & Cache", + "@settingsCache": { + "description": "Settings menu item - cache management" + }, + "settingsCacheSubtitle": "View size and clear cached data", + "@settingsCacheSubtitle": { + "description": "Subtitle for cache management menu" + }, + "libraryTitle": "Local Library", + "@libraryTitle": { + "description": "Library settings page title" + }, + "libraryScanSettings": "Scan Settings", + "@libraryScanSettings": { + "description": "Section header for scan settings" + }, + "libraryEnableLocalLibrary": "Enable Local Library", + "@libraryEnableLocalLibrary": { + "description": "Toggle to enable library scanning" + }, + "libraryEnableLocalLibrarySubtitle": "Scan and track your existing music", + "@libraryEnableLocalLibrarySubtitle": { + "description": "Subtitle for enable toggle" + }, + "libraryFolder": "Library Folder", + "@libraryFolder": { + "description": "Folder selection setting" + }, + "libraryFolderHint": "Tap to select folder", + "@libraryFolderHint": { + "description": "Placeholder when no folder selected" + }, + "libraryShowDuplicateIndicator": "Show Duplicate Indicator", + "@libraryShowDuplicateIndicator": { + "description": "Toggle for duplicate indicator in search" + }, + "libraryShowDuplicateIndicatorSubtitle": "Show when searching for existing tracks", + "@libraryShowDuplicateIndicatorSubtitle": { + "description": "Subtitle for duplicate indicator toggle" + }, + "libraryAutoScan": "Auto Scan", + "@libraryAutoScan": { + "description": "Setting for automatic library scanning" + }, + "libraryAutoScanSubtitle": "Automatically scan your library for new files", + "@libraryAutoScanSubtitle": { + "description": "Subtitle for auto scan setting" + }, + "libraryAutoScanOff": "Off", + "@libraryAutoScanOff": { + "description": "Auto scan disabled" + }, + "libraryAutoScanOnOpen": "Every app open", + "@libraryAutoScanOnOpen": { + "description": "Auto scan when app opens" + }, + "libraryAutoScanDaily": "Daily", + "@libraryAutoScanDaily": { + "description": "Auto scan once per day" + }, + "libraryAutoScanWeekly": "Weekly", + "@libraryAutoScanWeekly": { + "description": "Auto scan once per week" + }, + "libraryActions": "Actions", + "@libraryActions": { + "description": "Section header for library actions" + }, + "libraryScan": "Scan Library", + "@libraryScan": { + "description": "Button to start library scan" + }, + "libraryScanSubtitle": "Scan for audio files", + "@libraryScanSubtitle": { + "description": "Subtitle for scan button" + }, + "libraryScanSelectFolderFirst": "Select a folder first", + "@libraryScanSelectFolderFirst": { + "description": "Message when trying to scan without folder" + }, + "libraryCleanupMissingFiles": "Cleanup Missing Files", + "@libraryCleanupMissingFiles": { + "description": "Button to remove entries for missing files" + }, + "libraryCleanupMissingFilesSubtitle": "Remove entries for files that no longer exist", + "@libraryCleanupMissingFilesSubtitle": { + "description": "Subtitle for cleanup button" + }, + "libraryClear": "Clear Library", + "@libraryClear": { + "description": "Button to clear all library entries" + }, + "libraryClearSubtitle": "Remove all scanned tracks", + "@libraryClearSubtitle": { + "description": "Subtitle for clear button" + }, + "libraryClearConfirmTitle": "Clear Library", + "@libraryClearConfirmTitle": { + "description": "Dialog title for clear confirmation" + }, + "libraryClearConfirmMessage": "This will remove all scanned tracks from your library. Your actual music files will not be deleted.", + "@libraryClearConfirmMessage": { + "description": "Dialog message for clear confirmation" + }, + "libraryAbout": "About Local Library", + "@libraryAbout": { + "description": "Section header for about info" + }, + "libraryAboutDescription": "Scans your existing music collection to detect duplicates when downloading. Supports FLAC, M4A, MP3, Opus, and OGG formats. Metadata is read from file tags when available.", + "@libraryAboutDescription": { + "description": "Description of local library feature" + }, + "libraryTracksUnit": "{count, plural, =1{track} other{tracks}}", + "@libraryTracksUnit": { + "description": "Unit label for tracks count (without the number itself)", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryFilesUnit": "{count, plural, =1{file} other{files}}", + "@libraryFilesUnit": { + "description": "Unit label for files count during library scanning", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryLastScanned": "Last scanned: {time}", + "@libraryLastScanned": { + "description": "Last scan time display", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "libraryLastScannedNever": "Never", + "@libraryLastScannedNever": { + "description": "Shown when library has never been scanned" + }, + "libraryScanning": "Scanning...", + "@libraryScanning": { + "description": "Status during scan" + }, + "libraryScanFinalizing": "Finalizing library...", + "@libraryScanFinalizing": { + "description": "Status shown after file scanning finishes but library persistence is still running" + }, + "libraryScanProgress": "{progress}% of {total} files", + "@libraryScanProgress": { + "description": "Scan progress display", + "placeholders": { + "progress": { + "type": "String" + }, + "total": { + "type": "int" + } + } + }, + "libraryInLibrary": "In Library", + "@libraryInLibrary": { + "description": "Badge shown on tracks that exist in local library" + }, + "libraryRemovedMissingFiles": "Removed {count} missing files from library", + "@libraryRemovedMissingFiles": { + "description": "Snackbar after cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryCleared": "Library cleared", + "@libraryCleared": { + "description": "Snackbar after clearing library" + }, + "libraryStorageAccessRequired": "Storage Access Required", + "@libraryStorageAccessRequired": { + "description": "Dialog title for storage permission" + }, + "libraryStorageAccessMessage": "SpotiFLAC needs storage access to scan your music library. Please grant permission in settings.", + "@libraryStorageAccessMessage": { + "description": "Dialog message for storage permission" + }, + "libraryFolderNotExist": "Selected folder does not exist", + "@libraryFolderNotExist": { + "description": "Error when folder doesn't exist" + }, + "librarySourceDownloaded": "Downloaded", + "@librarySourceDownloaded": { + "description": "Badge for tracks downloaded via SpotiFLAC" + }, + "librarySourceLocal": "Local", + "@librarySourceLocal": { + "description": "Badge for tracks from local library scan" + }, + "libraryFilterAll": "All", + "@libraryFilterAll": { + "description": "Filter chip - show all library items" + }, + "libraryFilterDownloaded": "Downloaded", + "@libraryFilterDownloaded": { + "description": "Filter chip - show only downloaded items" + }, + "libraryFilterLocal": "Local", + "@libraryFilterLocal": { + "description": "Filter chip - show only local library items" + }, + "libraryFilterTitle": "Filters", + "@libraryFilterTitle": { + "description": "Filter bottom sheet title" + }, + "libraryFilterReset": "Reset", + "@libraryFilterReset": { + "description": "Reset all filters button" + }, + "libraryFilterApply": "Apply", + "@libraryFilterApply": { + "description": "Apply filters button" + }, + "libraryFilterSource": "Source", + "@libraryFilterSource": { + "description": "Filter section - source type" + }, + "libraryFilterQuality": "Quality", + "@libraryFilterQuality": { + "description": "Filter section - audio quality" + }, + "libraryFilterQualityHiRes": "Hi-Res (24bit)", + "@libraryFilterQualityHiRes": { + "description": "Filter option - high resolution audio" + }, + "libraryFilterQualityCD": "CD (16bit)", + "@libraryFilterQualityCD": { + "description": "Filter option - CD quality audio" + }, + "libraryFilterQualityLossy": "Lossy", + "@libraryFilterQualityLossy": { + "description": "Filter option - lossy compressed audio" + }, + "libraryFilterFormat": "Format", + "@libraryFilterFormat": { + "description": "Filter section - file format" + }, + "libraryFilterMetadata": "Metadata", + "@libraryFilterMetadata": { + "description": "Filter section - metadata completeness" + }, + "libraryFilterMetadataComplete": "Complete metadata", + "@libraryFilterMetadataComplete": { + "description": "Filter option - items with complete metadata" + }, + "libraryFilterMetadataMissingAny": "Missing any metadata", + "@libraryFilterMetadataMissingAny": { + "description": "Filter option - items missing any tracked metadata field" + }, + "libraryFilterMetadataMissingYear": "Missing year", + "@libraryFilterMetadataMissingYear": { + "description": "Filter option - items missing release year/date" + }, + "libraryFilterMetadataMissingGenre": "Missing genre", + "@libraryFilterMetadataMissingGenre": { + "description": "Filter option - items missing genre" + }, + "libraryFilterMetadataMissingAlbumArtist": "Missing album artist", + "@libraryFilterMetadataMissingAlbumArtist": { + "description": "Filter option - items missing album artist" + }, + "libraryFilterSort": "Sort", + "@libraryFilterSort": { + "description": "Filter section - sort order" + }, + "libraryFilterSortLatest": "Latest", + "@libraryFilterSortLatest": { + "description": "Sort option - newest first" + }, + "libraryFilterSortOldest": "Oldest", + "@libraryFilterSortOldest": { + "description": "Sort option - oldest first" + }, + "libraryFilterSortAlbumAsc": "Album (A-Z)", + "@libraryFilterSortAlbumAsc": { + "description": "Sort option - album ascending" + }, + "libraryFilterSortAlbumDesc": "Album (Z-A)", + "@libraryFilterSortAlbumDesc": { + "description": "Sort option - album descending" + }, + "libraryFilterSortGenreAsc": "Genre (A-Z)", + "@libraryFilterSortGenreAsc": { + "description": "Sort option - genre ascending" + }, + "libraryFilterSortGenreDesc": "Genre (Z-A)", + "@libraryFilterSortGenreDesc": { + "description": "Sort option - genre descending" + }, + "timeJustNow": "Just now", + "@timeJustNow": { + "description": "Relative time - less than a minute ago" + }, + "timeMinutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", + "@timeMinutesAgo": { + "description": "Relative time - minutes ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeHoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", + "@timeHoursAgo": { + "description": "Relative time - hours ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "tutorialWelcomeTitle": "Welcome to SpotiFLAC!", + "@tutorialWelcomeTitle": { + "description": "Tutorial welcome page title" + }, + "tutorialWelcomeDesc": "Let's learn how to download your favorite music in lossless quality. This quick tutorial will show you the basics.", + "@tutorialWelcomeDesc": { + "description": "Tutorial welcome page description" + }, + "tutorialWelcomeTip1": "Download music from Spotify, Deezer, or paste any supported URL", + "@tutorialWelcomeTip1": { + "description": "Tutorial welcome tip 1" + }, + "tutorialWelcomeTip2": "Get FLAC quality audio from Tidal, Qobuz, or Deezer", + "@tutorialWelcomeTip2": { + "description": "Tutorial welcome tip 2" + }, + "tutorialWelcomeTip3": "Automatic metadata, cover art, and lyrics embedding", + "@tutorialWelcomeTip3": { + "description": "Tutorial welcome tip 3" + }, + "tutorialSearchTitle": "Finding Music", + "@tutorialSearchTitle": { + "description": "Tutorial search page title" + }, + "tutorialSearchDesc": "There are two easy ways to find music you want to download.", + "@tutorialSearchDesc": { + "description": "Tutorial search page description" + }, + "tutorialDownloadTitle": "Downloading Music", + "@tutorialDownloadTitle": { + "description": "Tutorial download page title" + }, + "tutorialDownloadDesc": "Downloading music is simple and fast. Here's how it works.", + "@tutorialDownloadDesc": { + "description": "Tutorial download page description" + }, + "tutorialLibraryTitle": "Your Library", + "@tutorialLibraryTitle": { + "description": "Tutorial library page title" + }, + "tutorialLibraryDesc": "All your downloaded music is organized in the Library tab.", + "@tutorialLibraryDesc": { + "description": "Tutorial library page description" + }, + "tutorialLibraryTip1": "View download progress and queue in the Library tab", + "@tutorialLibraryTip1": { + "description": "Tutorial library tip 1" + }, + "tutorialLibraryTip2": "Tap any track to play it with your music player", + "@tutorialLibraryTip2": { + "description": "Tutorial library tip 2" + }, + "tutorialLibraryTip3": "Switch between list and grid view for better browsing", + "@tutorialLibraryTip3": { + "description": "Tutorial library tip 3" + }, + "tutorialExtensionsTitle": "Extensions", + "@tutorialExtensionsTitle": { + "description": "Tutorial extensions page title" + }, + "tutorialExtensionsDesc": "Extend the app's capabilities with community extensions.", + "@tutorialExtensionsDesc": { + "description": "Tutorial extensions page description" + }, + "tutorialExtensionsTip1": "Browse the Repo tab to discover useful extensions", + "@tutorialExtensionsTip1": { + "description": "Tutorial extensions tip 1" + }, + "tutorialExtensionsTip2": "Add new download providers or search sources", + "@tutorialExtensionsTip2": { + "description": "Tutorial extensions tip 2" + }, + "tutorialExtensionsTip3": "Get lyrics, enhanced metadata, and more features", + "@tutorialExtensionsTip3": { + "description": "Tutorial extensions tip 3" + }, + "tutorialSettingsTitle": "Customize Your Experience", + "@tutorialSettingsTitle": { + "description": "Tutorial settings page title" + }, + "tutorialSettingsDesc": "Personalize the app in Settings to match your preferences.", + "@tutorialSettingsDesc": { + "description": "Tutorial settings page description" + }, + "tutorialSettingsTip1": "Change download location and folder organization", + "@tutorialSettingsTip1": { + "description": "Tutorial settings tip 1" + }, + "tutorialSettingsTip2": "Set default audio quality and format preferences", + "@tutorialSettingsTip2": { + "description": "Tutorial settings tip 2" + }, + "tutorialSettingsTip3": "Customize app theme and appearance", + "@tutorialSettingsTip3": { + "description": "Tutorial settings tip 3" + }, + "tutorialReadyMessage": "You're all set! Start downloading your favorite music now.", + "@tutorialReadyMessage": { + "description": "Tutorial completion message" + }, + "libraryForceFullScan": "Force Full Scan", + "@libraryForceFullScan": { + "description": "Button to force a complete rescan of library" + }, + "libraryForceFullScanSubtitle": "Rescan all files, ignoring cache", + "@libraryForceFullScanSubtitle": { + "description": "Subtitle for force full scan button" + }, + "cleanupOrphanedDownloads": "Cleanup Orphaned Downloads", + "@cleanupOrphanedDownloads": { + "description": "Button to remove history entries for deleted files" + }, + "cleanupOrphanedDownloadsSubtitle": "Remove history entries for files that no longer exist", + "@cleanupOrphanedDownloadsSubtitle": { + "description": "Subtitle for orphaned cleanup button" + }, + "cleanupOrphanedDownloadsResult": "Removed {count} orphaned entries from history", + "@cleanupOrphanedDownloadsResult": { + "description": "Snackbar after orphan cleanup", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cleanupOrphanedDownloadsNone": "No orphaned entries found", + "@cleanupOrphanedDownloadsNone": { + "description": "Snackbar when no orphans found" + }, + "cacheTitle": "Storage & Cache", + "@cacheTitle": { + "description": "Cache management page title" + }, + "cacheSummaryTitle": "Cache overview", + "@cacheSummaryTitle": { + "description": "Heading for cache summary card" + }, + "cacheSummarySubtitle": "Clearing cache will not remove downloaded music files.", + "@cacheSummarySubtitle": { + "description": "Helper text for cache summary card" + }, + "cacheEstimatedTotal": "Estimated cache usage: {size}", + "@cacheEstimatedTotal": { + "description": "Total cache size shown in summary", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheSectionStorage": "Cached Data", + "@cacheSectionStorage": { + "description": "Section header for cache entries" + }, + "cacheSectionMaintenance": "Maintenance", + "@cacheSectionMaintenance": { + "description": "Section header for cleanup actions" + }, + "cacheAppDirectory": "App cache directory", + "@cacheAppDirectory": { + "description": "Cache item title for app cache directory" + }, + "cacheAppDirectoryDesc": "HTTP responses, WebView data, and other temporary app data.", + "@cacheAppDirectoryDesc": { + "description": "Description of what app cache directory contains" + }, + "cacheTempDirectory": "Temporary directory", + "@cacheTempDirectory": { + "description": "Cache item title for temporary files directory" + }, + "cacheTempDirectoryDesc": "Temporary files from downloads and audio conversion.", + "@cacheTempDirectoryDesc": { + "description": "Description of what temporary directory contains" + }, + "cacheCoverImage": "Cover image cache", + "@cacheCoverImage": { + "description": "Cache item title for persistent cover images" + }, + "cacheCoverImageDesc": "Downloaded album and track cover art. Will re-download when viewed.", + "@cacheCoverImageDesc": { + "description": "Description of what cover image cache contains" + }, + "cacheLibraryCover": "Library cover cache", + "@cacheLibraryCover": { + "description": "Cache item title for local library cover art images" + }, + "cacheLibraryCoverDesc": "Cover art extracted from local music files. Will re-extract on next scan.", + "@cacheLibraryCoverDesc": { + "description": "Description of what library cover cache contains" + }, + "cacheExploreFeed": "Explore feed cache", + "@cacheExploreFeed": { + "description": "Cache item title for explore home feed cache" + }, + "cacheExploreFeedDesc": "Explore tab content (new releases, trending). Will refresh on next visit.", + "@cacheExploreFeedDesc": { + "description": "Description of what explore feed cache contains" + }, + "cacheTrackLookup": "Track lookup cache", + "@cacheTrackLookup": { + "description": "Cache item title for track ID lookup cache" + }, + "cacheTrackLookupDesc": "Spotify/Deezer track ID lookups. Clearing may slow next few searches.", + "@cacheTrackLookupDesc": { + "description": "Description of what track lookup cache contains" + }, + "cacheCleanupUnusedDesc": "Remove orphaned download history and library entries for missing files.", + "@cacheCleanupUnusedDesc": { + "description": "Description of what cleanup unused data does" + }, + "cacheNoData": "No cached data", + "@cacheNoData": { + "description": "Label when cache category has no data" + }, + "cacheSizeWithFiles": "{size} in {count} files", + "@cacheSizeWithFiles": { + "description": "Cache size and file count", + "placeholders": { + "size": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cacheSizeOnly": "{size}", + "@cacheSizeOnly": { + "description": "Cache size only", + "placeholders": { + "size": { + "type": "String" + } + } + }, + "cacheEntries": "{count} entries", + "@cacheEntries": { + "description": "Track cache entry count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cacheClearSuccess": "Cleared: {target}", + "@cacheClearSuccess": { + "description": "Snackbar after clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearConfirmTitle": "Clear cache?", + "@cacheClearConfirmTitle": { + "description": "Dialog title before clearing one cache category" + }, + "cacheClearConfirmMessage": "This will clear cached data for {target}. Downloaded music files will not be deleted.", + "@cacheClearConfirmMessage": { + "description": "Dialog message before clearing selected cache", + "placeholders": { + "target": { + "type": "String" + } + } + }, + "cacheClearAllConfirmTitle": "Clear all cache?", + "@cacheClearAllConfirmTitle": { + "description": "Dialog title before clearing all caches" + }, + "cacheClearAllConfirmMessage": "This will clear all cache categories on this page. Downloaded music files will not be deleted.", + "@cacheClearAllConfirmMessage": { + "description": "Dialog message before clearing all caches" + }, + "cacheClearAll": "Clear all cache", + "@cacheClearAll": { + "description": "Button label to clear all caches" + }, + "cacheCleanupUnused": "Cleanup unused data", + "@cacheCleanupUnused": { + "description": "Action title for cleaning unused entries" + }, + "cacheCleanupUnusedSubtitle": "Remove orphaned download history and missing library entries", + "@cacheCleanupUnusedSubtitle": { + "description": "Subtitle for cleanup unused data action" + }, + "cacheCleanupResult": "Cleanup completed: {downloadCount} orphaned downloads, {libraryCount} missing library entries", + "@cacheCleanupResult": { + "description": "Snackbar after unused data cleanup", + "placeholders": { + "downloadCount": { + "type": "int" + }, + "libraryCount": { + "type": "int" + } + } + }, + "cacheRefreshStats": "Refresh stats", + "@cacheRefreshStats": { + "description": "Button label to refresh cache statistics" + }, + "trackSaveCoverArt": "Save Cover Art", + "@trackSaveCoverArt": { + "description": "Menu action - save album cover art as file" + }, + "trackSaveCoverArtSubtitle": "Save album art as .jpg file", + "@trackSaveCoverArtSubtitle": { + "description": "Subtitle for save cover art action" + }, + "trackSaveLyrics": "Save Lyrics (.lrc)", + "@trackSaveLyrics": { + "description": "Menu action - save lyrics as .lrc file" + }, + "trackSaveLyricsSubtitle": "Fetch and save lyrics as .lrc file", + "@trackSaveLyricsSubtitle": { + "description": "Subtitle for save lyrics action" + }, + "trackSaveLyricsProgress": "Saving lyrics...", + "@trackSaveLyricsProgress": { + "description": "Snackbar while saving lyrics to file" + }, + "trackReEnrich": "Re-enrich", + "@trackReEnrich": { + "description": "Menu action - re-embed metadata into audio file" + }, + "trackReEnrichOnlineSubtitle": "Search metadata online and embed into file", + "@trackReEnrichOnlineSubtitle": { + "description": "Subtitle for re-enrich metadata action for local items" + }, + "trackReEnrichFieldsTitle": "Fields to update", + "@trackReEnrichFieldsTitle": { + "description": "Section title for field selection in re-enrich dialog" + }, + "trackReEnrichFieldCover": "Cover Art", + "@trackReEnrichFieldCover": { + "description": "Checkbox label for cover art field in re-enrich" + }, + "trackReEnrichFieldLyrics": "Lyrics", + "@trackReEnrichFieldLyrics": { + "description": "Checkbox label for lyrics field in re-enrich" + }, + "trackReEnrichFieldBasicTags": "Album, Album Artist", + "@trackReEnrichFieldBasicTags": { + "description": "Checkbox label for basic tags in re-enrich (title/artist are never overwritten)" + }, + "trackReEnrichFieldTrackInfo": "Track & Disc Number", + "@trackReEnrichFieldTrackInfo": { + "description": "Checkbox label for track info in re-enrich" + }, + "trackReEnrichFieldReleaseInfo": "Date & ISRC", + "@trackReEnrichFieldReleaseInfo": { + "description": "Checkbox label for release info in re-enrich" + }, + "trackReEnrichFieldExtra": "Genre, Label, Copyright", + "@trackReEnrichFieldExtra": { + "description": "Checkbox label for extra metadata in re-enrich" + }, + "trackReEnrichSelectAll": "Select All", + "@trackReEnrichSelectAll": { + "description": "Select all fields checkbox in re-enrich" + }, + "trackEditMetadata": "Edit Metadata", + "@trackEditMetadata": { + "description": "Menu action - edit embedded metadata" + }, + "trackCoverSaved": "Cover art saved to {fileName}", + "@trackCoverSaved": { + "description": "Snackbar after cover art saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackCoverNoSource": "No cover art source available", + "@trackCoverNoSource": { + "description": "Snackbar when no cover art URL or embedded cover" + }, + "trackLyricsSaved": "Lyrics saved to {fileName}", + "@trackLyricsSaved": { + "description": "Snackbar after lyrics saved", + "placeholders": { + "fileName": { + "type": "String" + } + } + }, + "trackReEnrichProgress": "Re-enriching metadata...", + "@trackReEnrichProgress": { + "description": "Snackbar while re-enriching metadata" + }, + "trackReEnrichSearching": "Searching metadata online...", + "@trackReEnrichSearching": { + "description": "Snackbar while searching metadata from internet for local items" + }, + "trackReEnrichSuccess": "Metadata re-enriched successfully", + "@trackReEnrichSuccess": { + "description": "Snackbar after successful re-enrichment" + }, + "trackReEnrichFfmpegFailed": "FFmpeg metadata embed failed", + "@trackReEnrichFfmpegFailed": { + "description": "Snackbar when FFmpeg embed fails for MP3/Opus" + }, + "queueFlacAction": "Queue FLAC", + "@queueFlacAction": { + "description": "Action/button label for queueing FLAC redownloads for local tracks" + }, + "queueFlacConfirmMessage": "Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n{count} selected", + "@queueFlacConfirmMessage": { + "description": "Confirmation dialog body before queueing FLAC redownloads for local tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueFlacFindingProgress": "Finding FLAC matches... ({current}/{total})", + "@queueFlacFindingProgress": { + "description": "Snackbar while resolving remote matches for local FLAC redownloads", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "queueFlacNoReliableMatches": "No reliable online matches found for the selection", + "@queueFlacNoReliableMatches": { + "description": "Snackbar when no safe FLAC redownload matches were found" + }, + "queueFlacQueuedWithSkipped": "Added {addedCount} tracks to queue, skipped {skippedCount}", + "@queueFlacQueuedWithSkipped": { + "description": "Snackbar when some selected local tracks were queued for FLAC redownload and some were skipped", + "placeholders": { + "addedCount": { + "type": "int" + }, + "skippedCount": { + "type": "int" + } + } + }, + "trackSaveFailed": "Failed: {error}", + "@trackSaveFailed": { + "description": "Snackbar when save operation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "trackConvertFormat": "Convert Format", + "@trackConvertFormat": { + "description": "Menu item - convert audio format" + }, + "trackConvertFormatSubtitle": "Convert to MP3, Opus, ALAC, or FLAC", + "@trackConvertFormatSubtitle": { + "description": "Subtitle for convert format menu item" + }, + "trackConvertTitle": "Convert Audio", + "@trackConvertTitle": { + "description": "Title of convert bottom sheet" + }, + "trackConvertTargetFormat": "Target Format", + "@trackConvertTargetFormat": { + "description": "Label for format selection" + }, + "trackConvertBitrate": "Bitrate", + "@trackConvertBitrate": { + "description": "Label for bitrate selection" + }, + "trackConvertConfirmTitle": "Confirm Conversion", + "@trackConvertConfirmTitle": { + "description": "Confirmation dialog title" + }, + "trackConvertConfirmMessage": "Convert from {sourceFormat} to {targetFormat} at {bitrate}?\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessage": { + "description": "Confirmation dialog message", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "trackConvertConfirmMessageLossless": "Convert from {sourceFormat} to {targetFormat}? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.", + "@trackConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless-to-lossless conversion", + "placeholders": { + "sourceFormat": { + "type": "String" + }, + "targetFormat": { + "type": "String" + } + } + }, + "trackConvertLosslessHint": "Lossless conversion — no quality loss", + "@trackConvertLosslessHint": { + "description": "Hint shown when converting between lossless formats" + }, + "trackConvertConverting": "Converting audio...", + "@trackConvertConverting": { + "description": "Snackbar while converting" + }, + "trackConvertSuccess": "Converted to {format} successfully", + "@trackConvertSuccess": { + "description": "Snackbar after successful conversion", + "placeholders": { + "format": { + "type": "String" + } + } + }, + "trackConvertFailed": "Conversion failed", + "@trackConvertFailed": { + "description": "Snackbar when conversion fails" + }, + "cueSplitTitle": "Split CUE Sheet", + "@cueSplitTitle": { + "description": "Title for CUE split bottom sheet" + }, + "cueSplitSubtitle": "Split CUE+FLAC into individual tracks", + "@cueSplitSubtitle": { + "description": "Subtitle for CUE split menu item" + }, + "cueSplitAlbum": "Album: {album}", + "@cueSplitAlbum": { + "description": "Album name in CUE split sheet", + "placeholders": { + "album": { + "type": "String" + } + } + }, + "cueSplitArtist": "Artist: {artist}", + "@cueSplitArtist": { + "description": "Artist name in CUE split sheet", + "placeholders": { + "artist": { + "type": "String" + } + } + }, + "cueSplitTrackCount": "{count} tracks", + "@cueSplitTrackCount": { + "description": "Number of tracks in CUE sheet", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitConfirmTitle": "Split CUE Album", + "@cueSplitConfirmTitle": { + "description": "CUE split confirmation dialog title" + }, + "cueSplitConfirmMessage": "Split \"{album}\" into {count} individual FLAC files?\n\nFiles will be saved to the same directory.", + "@cueSplitConfirmMessage": { + "description": "CUE split confirmation dialog message", + "placeholders": { + "album": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "cueSplitSplitting": "Splitting CUE sheet... ({current}/{total})", + "@cueSplitSplitting": { + "description": "Snackbar while splitting CUE", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "cueSplitSuccess": "Split into {count} tracks successfully", + "@cueSplitSuccess": { + "description": "Snackbar after successful CUE split", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "cueSplitFailed": "CUE split failed", + "@cueSplitFailed": { + "description": "Snackbar when CUE split fails" + }, + "cueSplitNoAudioFile": "Audio file not found for this CUE sheet", + "@cueSplitNoAudioFile": { + "description": "Error when CUE audio file is missing" + }, + "cueSplitButton": "Split into Tracks", + "@cueSplitButton": { + "description": "Button text to start CUE splitting" + }, + "actionCreate": "Create", + "@actionCreate": { + "description": "Generic action button - create" + }, + "collectionFoldersTitle": "My folders", + "@collectionFoldersTitle": { + "description": "Library section title for custom folders" + }, + "collectionWishlist": "Wishlist", + "@collectionWishlist": { + "description": "Custom folder for saved tracks to download later" + }, + "collectionLoved": "Loved", + "@collectionLoved": { + "description": "Custom folder for favorite tracks" + }, + "collectionPlaylists": "Playlists", + "@collectionPlaylists": { + "description": "Custom user playlists folder" + }, + "collectionPlaylist": "Playlist", + "@collectionPlaylist": { + "description": "Single playlist label" + }, + "collectionAddToPlaylist": "Add to playlist", + "@collectionAddToPlaylist": { + "description": "Action to add a track to user playlist" + }, + "collectionCreatePlaylist": "Create playlist", + "@collectionCreatePlaylist": { + "description": "Action to create a new playlist" + }, + "collectionNoPlaylistsYet": "No playlists yet", + "@collectionNoPlaylistsYet": { + "description": "Empty state title when user has no playlists" + }, + "collectionNoPlaylistsSubtitle": "Create a playlist to start categorizing tracks", + "@collectionNoPlaylistsSubtitle": { + "description": "Empty state subtitle when user has no playlists" + }, + "collectionPlaylistTracks": "{count, plural, =1{1 track} other{{count} tracks}}", + "@collectionPlaylistTracks": { + "description": "Track count label for custom playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "collectionAddedToPlaylist": "Added to \"{playlistName}\"", + "@collectionAddedToPlaylist": { + "description": "Snackbar after adding track to playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionAlreadyInPlaylist": "Already in \"{playlistName}\"", + "@collectionAlreadyInPlaylist": { + "description": "Snackbar when track already exists in playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionPlaylistCreated": "Playlist created", + "@collectionPlaylistCreated": { + "description": "Snackbar after creating playlist" + }, + "collectionPlaylistNameHint": "Playlist name", + "@collectionPlaylistNameHint": { + "description": "Hint text for playlist name input" + }, + "collectionPlaylistNameRequired": "Playlist name is required", + "@collectionPlaylistNameRequired": { + "description": "Validation error for empty playlist name" + }, + "collectionRenamePlaylist": "Rename playlist", + "@collectionRenamePlaylist": { + "description": "Action to rename playlist" + }, + "collectionDeletePlaylist": "Delete playlist", + "@collectionDeletePlaylist": { + "description": "Action to delete playlist" + }, + "collectionDeletePlaylistMessage": "Delete \"{playlistName}\" and all tracks inside it?", + "@collectionDeletePlaylistMessage": { + "description": "Confirmation message for deleting playlist", + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "collectionPlaylistDeleted": "Playlist deleted", + "@collectionPlaylistDeleted": { + "description": "Snackbar after deleting playlist" + }, + "collectionPlaylistRenamed": "Playlist renamed", + "@collectionPlaylistRenamed": { + "description": "Snackbar after renaming playlist" + }, + "collectionWishlistEmptyTitle": "Wishlist is empty", + "@collectionWishlistEmptyTitle": { + "description": "Wishlist empty state title" + }, + "collectionWishlistEmptySubtitle": "Tap + on tracks to save what you want to download later", + "@collectionWishlistEmptySubtitle": { + "description": "Wishlist empty state subtitle" + }, + "collectionLovedEmptyTitle": "Loved folder is empty", + "@collectionLovedEmptyTitle": { + "description": "Loved empty state title" + }, + "collectionLovedEmptySubtitle": "Tap love on tracks to keep your favorites", + "@collectionLovedEmptySubtitle": { + "description": "Loved empty state subtitle" + }, + "collectionPlaylistEmptyTitle": "Playlist is empty", + "@collectionPlaylistEmptyTitle": { + "description": "Playlist empty state title" + }, + "collectionPlaylistEmptySubtitle": "Long-press + on any track to add it here", + "@collectionPlaylistEmptySubtitle": { + "description": "Playlist empty state subtitle" + }, + "collectionRemoveFromPlaylist": "Remove from playlist", + "@collectionRemoveFromPlaylist": { + "description": "Tooltip for removing track from playlist" + }, + "collectionRemoveFromFolder": "Remove from folder", + "@collectionRemoveFromFolder": { + "description": "Tooltip for removing track from wishlist/loved folder" + }, + "collectionRemoved": "\"{trackName}\" removed", + "@collectionRemoved": { + "description": "Snackbar after removing a track from a collection", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionAddedToLoved": "\"{trackName}\" added to Loved", + "@collectionAddedToLoved": { + "description": "Snackbar after adding track to loved folder", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionRemovedFromLoved": "\"{trackName}\" removed from Loved", + "@collectionRemovedFromLoved": { + "description": "Snackbar after removing track from loved folder", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionAddedToWishlist": "\"{trackName}\" added to Wishlist", + "@collectionAddedToWishlist": { + "description": "Snackbar after adding track to wishlist", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "collectionRemovedFromWishlist": "\"{trackName}\" removed from Wishlist", + "@collectionRemovedFromWishlist": { + "description": "Snackbar after removing track from wishlist", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "trackOptionAddToLoved": "Add to Loved", + "@trackOptionAddToLoved": { + "description": "Bottom sheet action label - add track to loved folder" + }, + "trackOptionRemoveFromLoved": "Remove from Loved", + "@trackOptionRemoveFromLoved": { + "description": "Bottom sheet action label - remove track from loved folder" + }, + "trackOptionAddToWishlist": "Add to Wishlist", + "@trackOptionAddToWishlist": { + "description": "Bottom sheet action label - add track to wishlist" + }, + "trackOptionRemoveFromWishlist": "Remove from Wishlist", + "@trackOptionRemoveFromWishlist": { + "description": "Bottom sheet action label - remove track from wishlist" + }, + "collectionPlaylistChangeCover": "Change cover image", + "@collectionPlaylistChangeCover": { + "description": "Bottom sheet action to pick a custom cover image for a playlist" + }, + "collectionPlaylistRemoveCover": "Remove cover image", + "@collectionPlaylistRemoveCover": { + "description": "Bottom sheet action to remove custom cover image from a playlist" + }, + "selectionShareCount": "Share {count} {count, plural, =1{track} other{tracks}}", + "@selectionShareCount": { + "description": "Share button text with count in selection mode", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionShareNoFiles": "No shareable files found", + "@selectionShareNoFiles": { + "description": "Snackbar when no selected files exist on disk" + }, + "selectionConvertCount": "Convert {count} {count, plural, =1{track} other{tracks}}", + "@selectionConvertCount": { + "description": "Convert button text with count in selection mode", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "selectionConvertNoConvertible": "No convertible tracks selected", + "@selectionConvertNoConvertible": { + "description": "Snackbar when no selected tracks support conversion" + }, + "selectionBatchConvertConfirmTitle": "Batch Convert", + "@selectionBatchConvertConfirmTitle": { + "description": "Confirmation dialog title for batch conversion" + }, + "selectionBatchConvertConfirmMessage": "Convert {count} {count, plural, =1{track} other{tracks}} to {format} at {bitrate}?\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessage": { + "description": "Confirmation dialog message for batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + }, + "bitrate": { + "type": "String" + } + } + }, + "selectionBatchConvertConfirmMessageLossless": "Convert {count} {count, plural, =1{track} other{tracks}} to {format}? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.", + "@selectionBatchConvertConfirmMessageLossless": { + "description": "Confirmation dialog message for lossless batch conversion", + "placeholders": { + "count": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, + "selectionBatchConvertProgress": "Converting {current} of {total}...", + "@selectionBatchConvertProgress": { + "description": "Snackbar during batch conversion progress", + "placeholders": { + "current": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "selectionBatchConvertSuccess": "Converted {success} of {total} tracks to {format}", + "@selectionBatchConvertSuccess": { + "description": "Snackbar after batch conversion completes", + "placeholders": { + "success": { + "type": "int" + }, + "total": { + "type": "int" + }, + "format": { + "type": "String" + } + } + }, + "downloadUseAlbumArtistForFoldersAlbumSubtitle": "Folder named after Album Artist tag", + "@downloadUseAlbumArtistForFoldersAlbumSubtitle": { + "description": "Subtitle when album artist is used for folder names" + }, + "downloadUseAlbumArtistForFoldersTrackSubtitle": "Folder named after Track Artist tag", + "@downloadUseAlbumArtistForFoldersTrackSubtitle": { + "description": "Subtitle when track artist is used for folder names" + }, + "lyricsProvidersTitle": "Lyrics Provider Priority", + "@lyricsProvidersTitle": { + "description": "Settings item title for lyrics provider order" + }, + "lyricsProvidersDescription": "Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.", + "@lyricsProvidersDescription": { + "description": "Description on the lyrics provider priority page" + }, + "lyricsProvidersInfoText": "Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.", + "@lyricsProvidersInfoText": { + "description": "Info tip on lyrics provider priority page" + }, + "lyricsProvidersEnabledSection": "Enabled ({count})", + "@lyricsProvidersEnabledSection": { + "description": "Section header for enabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersDisabledSection": "Disabled ({count})", + "@lyricsProvidersDisabledSection": { + "description": "Section header for disabled providers", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "lyricsProvidersAtLeastOne": "At least one provider must remain enabled", + "@lyricsProvidersAtLeastOne": { + "description": "Snackbar when user tries to disable the last enabled provider" + }, + "lyricsProvidersSaved": "Lyrics provider priority saved", + "@lyricsProvidersSaved": { + "description": "Snackbar after saving lyrics provider priority" + }, + "lyricsProvidersDiscardContent": "You have unsaved changes that will be lost.", + "@lyricsProvidersDiscardContent": { + "description": "Body text of the discard-changes dialog on lyrics provider page" + }, + "lyricsProviderLrclibDesc": "Open-source synced lyrics database", + "@lyricsProviderLrclibDesc": { + "description": "Description for LRCLIB provider" + }, + "lyricsProviderNeteaseDesc": "NetEase Cloud Music (good for Asian songs)", + "@lyricsProviderNeteaseDesc": { + "description": "Description for Netease provider" + }, + "lyricsProviderMusixmatchDesc": "Largest lyrics database (multi-language)", + "@lyricsProviderMusixmatchDesc": { + "description": "Description for Musixmatch provider" + }, + "lyricsProviderAppleMusicDesc": "Word-by-word synced lyrics (via proxy)", + "@lyricsProviderAppleMusicDesc": { + "description": "Description for Apple Music provider" + }, + "lyricsProviderQqMusicDesc": "QQ Music (good for Chinese songs, via proxy)", + "@lyricsProviderQqMusicDesc": { + "description": "Description for QQ Music provider" + }, + "lyricsProviderExtensionDesc": "Extension provider", + "@lyricsProviderExtensionDesc": { + "description": "Generic description for extension-based lyrics providers" + }, + "safMigrationTitle": "Storage Update Required", + "@safMigrationTitle": { + "description": "Title of SAF migration dialog" + }, + "safMigrationMessage1": "SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.", + "@safMigrationMessage1": { + "description": "First paragraph of SAF migration dialog" + }, + "safMigrationMessage2": "Please select your download folder again to switch to the new storage system.", + "@safMigrationMessage2": { + "description": "Second paragraph of SAF migration dialog" + }, + "safMigrationSuccess": "Download folder updated to SAF mode", + "@safMigrationSuccess": { + "description": "Snackbar after successfully migrating to SAF" + }, + "settingsDonate": "Support Development", + "@settingsDonate": { + "description": "Settings menu item - donate page" + }, + "settingsDonateSubtitle": "Buy the developer a coffee", + "@settingsDonateSubtitle": { + "description": "Subtitle for donate menu item" + }, + "tooltipLoveAll": "Love All", + "@tooltipLoveAll": { + "description": "Tooltip for the Love All button on album/playlist screens" + }, + "tooltipAddToPlaylist": "Add to Playlist", + "@tooltipAddToPlaylist": { + "description": "Tooltip for the Add to Playlist button" + }, + "snackbarRemovedTracksFromLoved": "Removed {count} tracks from Loved", + "@snackbarRemovedTracksFromLoved": { + "description": "Snackbar after removing multiple tracks from Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "snackbarAddedTracksToLoved": "Added {count} tracks to Loved", + "@snackbarAddedTracksToLoved": { + "description": "Snackbar after adding multiple tracks to Loved folder", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dialogDownloadAllTitle": "Download All", + "@dialogDownloadAllTitle": { + "description": "Dialog title for bulk download confirmation" + }, + "dialogDownloadAllMessage": "Download {count} tracks?", + "@dialogDownloadAllMessage": { + "description": "Body of the Download All confirmation dialog", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "homeSkipAlreadyDownloaded": "Skip already downloaded songs", + "@homeSkipAlreadyDownloaded": { + "description": "Checkbox label in import dialog to skip already-downloaded songs" + }, + "homeGoToAlbum": "Go to Album", + "@homeGoToAlbum": { + "description": "Context menu item to navigate to the album page" + }, + "homeAlbumInfoUnavailable": "Album info not available", + "@homeAlbumInfoUnavailable": { + "description": "Snackbar when album info cannot be loaded" + }, + "snackbarLoadingCueSheet": "Loading CUE sheet...", + "@snackbarLoadingCueSheet": { + "description": "Snackbar while loading a CUE sheet file" + }, + "snackbarMetadataSaved": "Metadata saved successfully", + "@snackbarMetadataSaved": { + "description": "Snackbar after successfully saving track metadata" + }, + "snackbarFailedToEmbedLyrics": "Failed to embed lyrics", + "@snackbarFailedToEmbedLyrics": { + "description": "Snackbar when lyrics embedding fails" + }, + "snackbarFailedToWriteStorage": "Failed to write back to storage", + "@snackbarFailedToWriteStorage": { + "description": "Snackbar when writing metadata back to file fails" + }, + "snackbarError": "Error: {error}", + "@snackbarError": { + "description": "Generic error snackbar with error detail", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "snackbarNoActionDefined": "No action defined for this button", + "@snackbarNoActionDefined": { + "description": "Snackbar when an extension button has no action configured" + }, + "noTracksFoundForAlbum": "No tracks found for this album", + "@noTracksFoundForAlbum": { + "description": "Empty state message when an album has no tracks" + }, + "downloadLocationSubtitle": "Choose where to save your downloaded tracks", + "@downloadLocationSubtitle": { + "description": "Subtitle shown in the download location picker sheet" + }, + "storageModeAppFolder": "App Folder (Recommended)", + "@storageModeAppFolder": { + "description": "Storage mode option - app-managed folder" + }, + "storageModeAppFolderSubtitle": "Saves to Music/SpotiFLAC by default", + "@storageModeAppFolderSubtitle": { + "description": "Subtitle for app folder storage mode" + }, + "storageModeSaf": "Custom Folder (SAF)", + "@storageModeSaf": { + "description": "Storage mode option - Storage Access Framework" + }, + "storageModeSafSubtitle": "Pick any folder, including SD card", + "@storageModeSafSubtitle": { + "description": "Subtitle for SAF storage mode" + }, + "downloadFilenameDescription": "Use {artist}, {title}, {album}, {track}, {year}, {date}, {disc} as placeholders.", + "@downloadFilenameDescription": { + "description": "Description shown in filename format editor" + }, + "downloadFilenameInsertTag": "Tap to insert tag:", + "@downloadFilenameInsertTag": { + "description": "Label above filename tag chips" + }, + "downloadSeparateSinglesEnabled": "Singles and EPs saved in a separate folder", + "@downloadSeparateSinglesEnabled": { + "description": "Subtitle when separate singles folder is on" + }, + "downloadSeparateSinglesDisabled": "Singles and albums saved in the same folder", + "@downloadSeparateSinglesDisabled": { + "description": "Subtitle when separate singles folder is off" + }, + "downloadArtistNameFilters": "Artist Name Filters", + "@downloadArtistNameFilters": { + "description": "Setting title for artist folder filter options" + }, + "downloadCreatePlaylistSourceFolder": "Playlist Source Folder", + "@downloadCreatePlaylistSourceFolder": { + "description": "Setting to create a subfolder per playlist source" + }, + "downloadCreatePlaylistSourceFolderEnabled": "A subfolder is created for each playlist", + "@downloadCreatePlaylistSourceFolderEnabled": { + "description": "Subtitle when playlist folder is enabled" + }, + "downloadCreatePlaylistSourceFolderDisabled": "All tracks saved directly to download folder", + "@downloadCreatePlaylistSourceFolderDisabled": { + "description": "Subtitle when playlist folder is disabled" + }, + "downloadCreatePlaylistSourceFolderRedundant": "Handled by folder organization setting", + "@downloadCreatePlaylistSourceFolderRedundant": { + "description": "Subtitle when folder organization is already set to playlist" + }, + "downloadSongLinkRegion": "SongLink Region", + "@downloadSongLinkRegion": { + "description": "Setting for SongLink region used during fallback resolution" + }, + "downloadNetworkCompatibilityMode": "Network Compatibility Mode", + "@downloadNetworkCompatibilityMode": { + "description": "Setting for legacy TLS/network handling" + }, + "downloadNetworkCompatibilityModeEnabled": "Using legacy TLS settings for older networks", + "@downloadNetworkCompatibilityModeEnabled": { + "description": "Subtitle when network compatibility mode is on" + }, + "downloadNetworkCompatibilityModeDisabled": "Using standard network settings", + "@downloadNetworkCompatibilityModeDisabled": { + "description": "Subtitle when network compatibility mode is off" + }, + "downloadSelectServiceToEnable": "Select Tidal or Qobuz to enable this option", + "@downloadSelectServiceToEnable": { + "description": "Subtitle when quality picker is disabled due to extension service" + }, + "downloadSelectTidalQobuz": "Select Tidal or Qobuz to choose audio quality", + "@downloadSelectTidalQobuz": { + "description": "Info shown when a non-built-in service is selected" + }, + "downloadEmbedLyricsDisabled": "Enable metadata embedding first", + "@downloadEmbedLyricsDisabled": { + "description": "Subtitle when lyrics embedding is blocked by metadata toggle" + }, + "downloadNeteaseIncludeTranslation": "Netease: Include Translation", + "@downloadNeteaseIncludeTranslation": { + "description": "Setting to include translated lyrics from Netease" + }, + "downloadNeteaseIncludeTranslationEnabled": "Chinese translation lines included", + "@downloadNeteaseIncludeTranslationEnabled": { + "description": "Subtitle when Netease translation is on" + }, + "downloadNeteaseIncludeTranslationDisabled": "Original lyrics only", + "@downloadNeteaseIncludeTranslationDisabled": { + "description": "Subtitle when Netease translation is off" + }, + "downloadNeteaseIncludeRomanization": "Netease: Include Romanization", + "@downloadNeteaseIncludeRomanization": { + "description": "Setting to include romanized lyrics from Netease" + }, + "downloadNeteaseIncludeRomanizationEnabled": "Romanization lines included", + "@downloadNeteaseIncludeRomanizationEnabled": { + "description": "Subtitle when Netease romanization is on" + }, + "downloadNeteaseIncludeRomanizationDisabled": "No romanization", + "@downloadNeteaseIncludeRomanizationDisabled": { + "description": "Subtitle when Netease romanization is off" + }, + "downloadAppleQqMultiPerson": "Apple / QQ: Multi-Person Lyrics", + "@downloadAppleQqMultiPerson": { + "description": "Setting for word-by-word multi-person lyrics from Apple Music and QQ Music" + }, + "downloadAppleQqMultiPersonEnabled": "Speaker labels included for duets and group tracks", + "@downloadAppleQqMultiPersonEnabled": { + "description": "Subtitle when multi-person lyrics is on" + }, + "downloadAppleQqMultiPersonDisabled": "Standard lyrics without speaker labels", + "@downloadAppleQqMultiPersonDisabled": { + "description": "Subtitle when multi-person lyrics is off" + }, + "downloadMusixmatchLanguage": "Musixmatch Language", + "@downloadMusixmatchLanguage": { + "description": "Setting for Musixmatch lyrics translation language" + }, + "downloadMusixmatchLanguageAuto": "Auto (original language)", + "@downloadMusixmatchLanguageAuto": { + "description": "Subtitle when no language is set" + }, + "downloadFilterContributing": "Filter Contributing Artists", + "@downloadFilterContributing": { + "description": "Setting to strip contributing artists from Album Artist folder name" + }, + "downloadFilterContributingEnabled": "Contributing artists removed from Album Artist folder name", + "@downloadFilterContributingEnabled": { + "description": "Subtitle when contributing artist filter is on" + }, + "downloadFilterContributingDisabled": "Full Album Artist string used", + "@downloadFilterContributingDisabled": { + "description": "Subtitle when contributing artist filter is off" + }, + "downloadProvidersNoneEnabled": "No providers enabled", + "@downloadProvidersNoneEnabled": { + "description": "Shown when no lyrics providers are active" + }, + "downloadMusixmatchLanguageCode": "Language code", + "@downloadMusixmatchLanguageCode": { + "description": "Label for Musixmatch language input field" + }, + "downloadMusixmatchLanguageHint": "e.g. en, de, ja", + "@downloadMusixmatchLanguageHint": { + "description": "Placeholder for Musixmatch language input" + }, + "downloadMusixmatchLanguageDesc": "Enter a BCP-47 language code (e.g. en, de, ja) to request translated lyrics from Musixmatch.", + "@downloadMusixmatchLanguageDesc": { + "description": "Description in Musixmatch language picker" + }, + "downloadMusixmatchAuto": "Auto", + "@downloadMusixmatchAuto": { + "description": "Button to clear Musixmatch language (use auto)" + }, + "downloadNetworkAnySubtitle": "Use WiFi or mobile data", + "@downloadNetworkAnySubtitle": { + "description": "Subtitle for any-network option in picker" + }, + "downloadNetworkWifiOnlySubtitle": "Downloads pause when on mobile data", + "@downloadNetworkWifiOnlySubtitle": { + "description": "Subtitle for WiFi-only option in picker" + }, + "downloadSongLinkRegionDesc": "Region used when resolving track links via SongLink. Choose the country where your streaming services are available.", + "@downloadSongLinkRegionDesc": { + "description": "Description in SongLink region picker" + }, + "snackbarUnsupportedAudioFormat": "Unsupported audio format", + "@snackbarUnsupportedAudioFormat": { + "description": "Snackbar when the audio format is not supported for the requested operation" + }, + "cacheRefresh": "Refresh", + "@cacheRefresh": { + "description": "Tooltip for refresh button on cache management page" + }, + "dialogDownloadPlaylistsMessage": "Download {trackCount} {trackCount, plural, =1{track} other{tracks}} from {playlistCount} {playlistCount, plural, =1{playlist} other{playlists}}?", + "@dialogDownloadPlaylistsMessage": { + "description": "Dialog message for bulk playlist download confirmation", + "placeholders": { + "trackCount": { + "type": "int" + }, + "playlistCount": { + "type": "int" + } + } + }, + "bulkDownloadPlaylistsButton": "Download {count} {count, plural, =1{playlist} other{playlists}}", + "@bulkDownloadPlaylistsButton": { + "description": "Button label for bulk downloading selected playlists", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "bulkDownloadSelectPlaylists": "Select playlists to download", + "@bulkDownloadSelectPlaylists": { + "description": "Button label when no playlists are selected for download" + }, + "snackbarSelectedPlaylistsEmpty": "Selected playlists have no tracks", + "@snackbarSelectedPlaylistsEmpty": { + "description": "Snackbar when selected playlists contain no tracks" + }, + "playlistsCount": "{count, plural, =1{1 playlist} other{{count} playlists}}", + "@playlistsCount": { + "description": "Playlist count display", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFill": "Auto-fill from online", + "@editMetadataAutoFill": { + "description": "Section title for selective online metadata auto-fill in the edit metadata sheet" + }, + "editMetadataAutoFillDesc": "Select fields to fill automatically from online metadata", + "@editMetadataAutoFillDesc": { + "description": "Description for the auto-fill section" + }, + "editMetadataAutoFillFetch": "Fetch & Fill", + "@editMetadataAutoFillFetch": { + "description": "Button label to fetch online metadata and fill selected fields" + }, + "editMetadataAutoFillSearching": "Searching online...", + "@editMetadataAutoFillSearching": { + "description": "Snackbar shown while searching for online metadata" + }, + "editMetadataAutoFillNoResults": "No matching metadata found online", + "@editMetadataAutoFillNoResults": { + "description": "Snackbar when online metadata search returns no results" + }, + "editMetadataAutoFillDone": "Filled {count} {count, plural, =1{field} other{fields}} from online metadata", + "@editMetadataAutoFillDone": { + "description": "Snackbar confirming how many fields were auto-filled", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFillNoneSelected": "Select at least one field to auto-fill", + "@editMetadataAutoFillNoneSelected": { + "description": "Snackbar when user taps Fetch without selecting any fields" + }, + "editMetadataFieldTitle": "Title", + "@editMetadataFieldTitle": { + "description": "Chip label for title field in auto-fill selector" + }, + "editMetadataFieldArtist": "Artist", + "@editMetadataFieldArtist": { + "description": "Chip label for artist field in auto-fill selector" + }, + "editMetadataFieldAlbum": "Album", + "@editMetadataFieldAlbum": { + "description": "Chip label for album field in auto-fill selector" + }, + "editMetadataFieldAlbumArtist": "Album Artist", + "@editMetadataFieldAlbumArtist": { + "description": "Chip label for album artist field in auto-fill selector" + }, + "editMetadataFieldDate": "Date", + "@editMetadataFieldDate": { + "description": "Chip label for date field in auto-fill selector" + }, + "editMetadataFieldTrackNum": "Track #", + "@editMetadataFieldTrackNum": { + "description": "Chip label for track number field in auto-fill selector" + }, + "editMetadataFieldDiscNum": "Disc #", + "@editMetadataFieldDiscNum": { + "description": "Chip label for disc number field in auto-fill selector" + }, + "editMetadataFieldGenre": "Genre", + "@editMetadataFieldGenre": { + "description": "Chip label for genre field in auto-fill selector" + }, + "editMetadataFieldIsrc": "ISRC", + "@editMetadataFieldIsrc": { + "description": "Chip label for ISRC field in auto-fill selector" + }, + "editMetadataFieldLabel": "Label", + "@editMetadataFieldLabel": { + "description": "Chip label for label field in auto-fill selector" + }, + "editMetadataFieldCopyright": "Copyright", + "@editMetadataFieldCopyright": { + "description": "Chip label for copyright field in auto-fill selector" + }, + "editMetadataFieldCover": "Cover Art", + "@editMetadataFieldCover": { + "description": "Chip label for cover art field in auto-fill selector" + }, + "editMetadataSelectAll": "All", + "@editMetadataSelectAll": { + "description": "Button to select all fields for auto-fill" + }, + "editMetadataSelectEmpty": "Empty only", + "@editMetadataSelectEmpty": { + "description": "Button to select only fields that are currently empty" + }, + "queueDownloadingCount": "Downloading ({count})", + "@queueDownloadingCount": { + "description": "Header for active downloads section with count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueDownloadedHeader": "Downloaded", + "@queueDownloadedHeader": { + "description": "Header label for downloaded items section in library" + }, + "queueFilteringIndicator": "Filtering...", + "@queueFilteringIndicator": { + "description": "Shown while filter results are being computed" + }, + "queueTrackCount": "{count, plural, =1{1 track} other{{count} tracks}}", + "@queueTrackCount": { + "description": "Track count label with plural support", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueAlbumCount": "{count, plural, =1{1 album} other{{count} albums}}", + "@queueAlbumCount": { + "description": "Album count label with plural support", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "queueEmptyAlbums": "No album downloads", + "@queueEmptyAlbums": { + "description": "Empty state title when no album downloads exist" + }, + "queueEmptyAlbumsSubtitle": "Download multiple tracks from an album to see them here", + "@queueEmptyAlbumsSubtitle": { + "description": "Empty state subtitle for album downloads" + }, + "queueEmptySingles": "No single downloads", + "@queueEmptySingles": { + "description": "Empty state title when no single track downloads exist" + }, + "queueEmptySinglesSubtitle": "Single track downloads will appear here", + "@queueEmptySinglesSubtitle": { + "description": "Empty state subtitle for single track downloads" + }, + "queueEmptyHistory": "No download history", + "@queueEmptyHistory": { + "description": "Empty state title when download history is empty" + }, + "queueEmptyHistorySubtitle": "Downloaded tracks will appear here", + "@queueEmptyHistorySubtitle": { + "description": "Empty state subtitle for download history" + }, + "selectionAllPlaylistsSelected": "All playlists selected", + "@selectionAllPlaylistsSelected": { + "description": "Shown when all playlists are selected in selection mode" + }, + "selectionTapPlaylistsToSelect": "Tap playlists to select", + "@selectionTapPlaylistsToSelect": { + "description": "Hint shown in playlist selection mode" + }, + "selectionSelectPlaylistsToDelete": "Select playlists to delete", + "@selectionSelectPlaylistsToDelete": { + "description": "Hint shown when no playlists are selected for deletion" + }, + "audioAnalysisTitle": "Audio Quality Analysis", + "@audioAnalysisTitle": { + "description": "Title for audio analysis section" + }, + "audioAnalysisDescription": "Verify lossless quality with spectrum analysis", + "@audioAnalysisDescription": { + "description": "Description for audio analysis tap-to-analyze prompt" + }, + "audioAnalysisAnalyzing": "Analyzing audio...", + "@audioAnalysisAnalyzing": { + "description": "Loading text while analyzing audio" + }, + "audioAnalysisSampleRate": "Sample Rate", + "@audioAnalysisSampleRate": { + "description": "Sample rate metric label" + }, + "audioAnalysisBitDepth": "Bit Depth", + "@audioAnalysisBitDepth": { + "description": "Bit depth metric label" + }, + "audioAnalysisChannels": "Channels", + "@audioAnalysisChannels": { + "description": "Channels metric label" + }, + "audioAnalysisDuration": "Duration", + "@audioAnalysisDuration": { + "description": "Duration metric label" + }, + "audioAnalysisNyquist": "Nyquist", + "@audioAnalysisNyquist": { + "description": "Nyquist frequency metric label" + }, + "audioAnalysisFileSize": "Size", + "@audioAnalysisFileSize": { + "description": "File size metric label" + }, + "audioAnalysisDynamicRange": "Dynamic Range", + "@audioAnalysisDynamicRange": { + "description": "Dynamic range metric label" + }, + "audioAnalysisPeak": "Peak", + "@audioAnalysisPeak": { + "description": "Peak amplitude metric label" + }, + "audioAnalysisRms": "RMS", + "@audioAnalysisRms": { + "description": "RMS level metric label" + }, + "audioAnalysisSamples": "Samples", + "@audioAnalysisSamples": { + "description": "Total samples metric label" + }, + "extensionsSearchWith": "Search with {providerName}", + "@extensionsSearchWith": { + "description": "Extensions page - subtitle for built-in search provider option", + "placeholders": { + "providerName": { + "type": "String" + } + } + }, + "extensionsHomeFeedProvider": "Home Feed Provider", + "@extensionsHomeFeedProvider": { + "description": "Extensions page - label for home feed provider selector" + }, + "extensionsHomeFeedDescription": "Choose which extension provides the home feed on the main screen", + "@extensionsHomeFeedDescription": { + "description": "Extensions page - description for home feed provider picker" + }, + "extensionsHomeFeedAuto": "Auto", + "@extensionsHomeFeedAuto": { + "description": "Label for auto-selected search provider" + }, + "extensionsHomeFeedAutoSubtitle": "Automatically select the best available", + "@extensionsHomeFeedAutoSubtitle": { + "description": "Extensions page - subtitle for auto home feed option" + }, + "extensionsHomeFeedUse": "Use {extensionName} home feed", + "@extensionsHomeFeedUse": { + "description": "Extensions page - subtitle for a specific extension home feed option", + "placeholders": { + "extensionName": { + "type": "String" + } + } + }, + "extensionsNoHomeFeedExtensions": "No extensions with home feed", + "@extensionsNoHomeFeedExtensions": { + "description": "Extensions page - shown when no installed extension has home feed" + }, + "sortAlphaAsc": "A-Z", + "@sortAlphaAsc": { + "description": "Sort option - alphabetical ascending" + }, + "sortAlphaDesc": "Z-A", + "@sortAlphaDesc": { + "description": "Sort option - alphabetical descending" + }, + "cancelDownloadTitle": "Cancel download?", + "@cancelDownloadTitle": { + "description": "Dialog title when confirming cancellation of an active download" + }, + "cancelDownloadContent": "This will cancel the active download for \"{trackName}\".", + "@cancelDownloadContent": { + "description": "Dialog body when confirming cancellation of an active download", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "cancelDownloadKeep": "Keep", + "@cancelDownloadKeep": { + "description": "Dialog button - keep the active download (do not cancel)" + }, + "metadataSaveFailedFfmpeg": "Failed to save metadata via FFmpeg", + "@metadataSaveFailedFfmpeg": { + "description": "Snackbar error when FFmpeg fails to write metadata" + }, + "metadataSaveFailedStorage": "Failed to write metadata back to storage", + "@metadataSaveFailedStorage": { + "description": "Snackbar error when writing metadata file back to storage fails" + }, + "snackbarFolderPickerFailed": "Failed to open folder picker: {error}", + "@snackbarFolderPickerFailed": { + "description": "Snackbar shown when folder picker fails to open", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "errorLoadAlbum": "Failed to load album", + "@errorLoadAlbum": { + "description": "Error state shown when album fails to load" + }, + "errorLoadPlaylist": "Failed to load playlist", + "@errorLoadPlaylist": { + "description": "Error state shown when playlist fails to load" + }, + "errorLoadArtist": "Failed to load artist", + "@errorLoadArtist": { + "description": "Error state shown when artist fails to load" + }, + "notifChannelDownloadName": "Download Progress", + "@notifChannelDownloadName": { + "description": "Android notification channel name for download progress" + }, + "notifChannelDownloadDesc": "Shows download progress for tracks", + "@notifChannelDownloadDesc": { + "description": "Android notification channel description for download progress" + }, + "notifChannelLibraryScanName": "Library Scan", + "@notifChannelLibraryScanName": { + "description": "Android notification channel name for library scan" + }, + "notifChannelLibraryScanDesc": "Shows local library scan progress", + "@notifChannelLibraryScanDesc": { + "description": "Android notification channel description for library scan" + }, + "notifDownloadingTrack": "Downloading {trackName}", + "@notifDownloadingTrack": { + "description": "Notification title while downloading a track", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "notifFinalizingTrack": "Finalizing {trackName}", + "@notifFinalizingTrack": { + "description": "Notification title while finalizing (embedding metadata) a track", + "placeholders": { + "trackName": { + "type": "String" + } + } + }, + "notifEmbeddingMetadata": "Embedding metadata...", + "@notifEmbeddingMetadata": { + "description": "Notification body while embedding metadata into a downloaded track" + }, + "notifAlreadyInLibraryCount": "Already in Library ({completed}/{total})", + "@notifAlreadyInLibraryCount": { + "description": "Notification title when track is already in library, with count", + "placeholders": { + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "notifAlreadyInLibrary": "Already in Library", + "@notifAlreadyInLibrary": { + "description": "Notification title when track is already in library" + }, + "notifDownloadCompleteCount": "Download Complete ({completed}/{total})", + "@notifDownloadCompleteCount": { + "description": "Notification title when download is complete, with count", + "placeholders": { + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "notifDownloadComplete": "Download Complete", + "@notifDownloadComplete": { + "description": "Notification title when a single download is complete" + }, + "notifDownloadsFinished": "Downloads Finished ({completed} done, {failed} failed)", + "@notifDownloadsFinished": { + "description": "Notification title when queue finishes with some failures", + "placeholders": { + "completed": { + "type": "int" + }, + "failed": { + "type": "int" + } + } + }, + "notifAllDownloadsComplete": "All Downloads Complete", + "@notifAllDownloadsComplete": { + "description": "Notification title when all downloads finish successfully" + }, + "notifTracksDownloadedSuccess": "{count} tracks downloaded successfully", + "@notifTracksDownloadedSuccess": { + "description": "Notification body for queue complete - how many tracks were downloaded", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifScanningLibrary": "Scanning local library", + "@notifScanningLibrary": { + "description": "Notification title while scanning local library" + }, + "notifLibraryScanProgressWithTotal": "{scanned}/{total} files • {percentage}%", + "@notifLibraryScanProgressWithTotal": { + "description": "Notification body for library scan progress when total is known", + "placeholders": { + "scanned": { + "type": "int" + }, + "total": { + "type": "int" + }, + "percentage": { + "type": "int" + } + } + }, + "notifLibraryScanProgressNoTotal": "{scanned} files scanned • {percentage}%", + "@notifLibraryScanProgressNoTotal": { + "description": "Notification body for library scan progress when total is unknown", + "placeholders": { + "scanned": { + "type": "int" + }, + "percentage": { + "type": "int" + } + } + }, + "notifLibraryScanComplete": "Library scan complete", + "@notifLibraryScanComplete": { + "description": "Notification title when library scan finishes" + }, + "notifLibraryScanCompleteBody": "{count} tracks indexed", + "@notifLibraryScanCompleteBody": { + "description": "Notification body for library scan complete - number of indexed tracks", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanExcluded": "{count} excluded", + "@notifLibraryScanExcluded": { + "description": "Library scan complete suffix - excluded track count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanErrors": "{count} errors", + "@notifLibraryScanErrors": { + "description": "Library scan complete suffix - error count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "notifLibraryScanFailed": "Library scan failed", + "@notifLibraryScanFailed": { + "description": "Notification title when library scan fails" + }, + "notifLibraryScanCancelled": "Library scan cancelled", + "@notifLibraryScanCancelled": { + "description": "Notification title when library scan is cancelled by the user" + }, + "notifLibraryScanStopped": "Scan stopped before completion.", + "@notifLibraryScanStopped": { + "description": "Notification body when library scan is cancelled" + }, + "notifDownloadingUpdate": "Downloading SpotiFLAC v{version}", + "@notifDownloadingUpdate": { + "description": "Notification title while downloading an app update", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "notifUpdateProgress": "{received} / {total} MB • {percentage}%", + "@notifUpdateProgress": { + "description": "Notification body showing update download progress", + "placeholders": { + "received": { + "type": "String" + }, + "total": { + "type": "String" + }, + "percentage": { + "type": "int" + } + } + }, + "notifUpdateReady": "Update Ready", + "@notifUpdateReady": { + "description": "Notification title when app update download is complete" + }, + "notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.", + "@notifUpdateReadyBody": { + "description": "Notification body when app update is ready to install", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "notifUpdateFailed": "Update Failed", + "@notifUpdateFailed": { + "description": "Notification title when app update download fails" + }, + "notifUpdateFailedBody": "Could not download update. Try again later.", + "@notifUpdateFailedBody": { + "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb index 5b271364..cd341cef 100644 --- a/lib/l10n/arb/app_zh_CN.arb +++ b/lib/l10n/arb/app_zh_CN.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } diff --git a/lib/l10n/arb/app_zh_TW.arb b/lib/l10n/arb/app_zh_TW.arb index 4d9a042a..139262bf 100644 --- a/lib/l10n/arb/app_zh_TW.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -4509,5 +4509,77 @@ "notifUpdateFailedBody": "Could not download update. Try again later.", "@notifUpdateFailedBody": { "description": "Notification body when app update download fails" + }, + "settingsFiles": "Files & Folders", + "@settingsFiles": { + "description": "Settings menu item - file and folder settings" + }, + "settingsFilesSubtitle": "Download location, filename, folder structure", + "@settingsFilesSubtitle": { + "description": "Subtitle for files & folders settings" + }, + "settingsMetadata": "Metadata", + "@settingsMetadata": { + "description": "Settings menu item - metadata settings" + }, + "settingsMetadataSubtitle": "Cover art, tags, ReplayGain, providers", + "@settingsMetadataSubtitle": { + "description": "Subtitle for metadata settings" + }, + "settingsLyrics": "Lyrics", + "@settingsLyrics": { + "description": "Settings menu item - lyrics settings" + }, + "settingsLyricsSubtitle": "Embed, mode, providers, language options", + "@settingsLyricsSubtitle": { + "description": "Subtitle for lyrics settings" + }, + "settingsApp": "App", + "@settingsApp": { + "description": "Settings menu item - app settings" + }, + "settingsAppSubtitle": "Updates, data, extension repo, debug", + "@settingsAppSubtitle": { + "description": "Subtitle for app settings" + }, + "sectionMetadataProviders": "Providers", + "@sectionMetadataProviders": { + "description": "Settings section header for metadata providers" + }, + "sectionDuplicates": "Duplicates", + "@sectionDuplicates": { + "description": "Settings section header for deduplication" + }, + "sectionLyricsProviderOptions": "Provider Options", + "@sectionLyricsProviderOptions": { + "description": "Settings section header for per-provider lyrics options" + }, + "metadataProvidersTitle": "Metadata Provider Priority", + "@metadataProvidersTitle": { + "description": "Settings item title for metadata provider order" + }, + "metadataProvidersSubtitle": "Drag to set search and metadata source order", + "@metadataProvidersSubtitle": { + "description": "Subtitle for metadata provider priority item" + }, + "downloadDeduplication": "Skip Duplicate Downloads", + "@downloadDeduplication": { + "description": "Setting - skip tracks already in download history" + }, + "downloadDeduplicationEnabled": "Already-downloaded tracks will be skipped", + "@downloadDeduplicationEnabled": { + "description": "Subtitle when deduplication is on" + }, + "downloadDeduplicationDisabled": "All tracks will be downloaded regardless of history", + "@downloadDeduplicationDisabled": { + "description": "Subtitle when deduplication is off" + }, + "downloadFallbackExtensions": "Fallback Extensions", + "@downloadFallbackExtensions": { + "description": "Settings item for configuring fallback extension providers" + }, + "downloadFallbackExtensionsSubtitle": "Choose which extensions can be used as fallback", + "@downloadFallbackExtensionsSubtitle": { + "description": "Subtitle for fallback extensions item" } } diff --git a/lib/screens/settings/app_settings_page.dart b/lib/screens/settings/app_settings_page.dart new file mode 100644 index 00000000..8b7d14bf --- /dev/null +++ b/lib/screens/settings/app_settings_page.dart @@ -0,0 +1,372 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/providers/download_queue_provider.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/app_bar_layout.dart'; +import 'package:spotiflac_android/widgets/settings_group.dart'; + +class AppSettingsPage extends ConsumerWidget { + const AppSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final colorScheme = Theme.of(context).colorScheme; + final topPadding = normalizedHeaderTopPadding(context); + + return PopScope( + canPop: true, + child: Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 120 + topPadding, + collapsedHeight: kToolbarHeight, + floating: false, + pinned: true, + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final maxHeight = 120 + topPadding; + final minHeight = kToolbarHeight + topPadding; + final expandRatio = + ((constraints.maxHeight - minHeight) / + (maxHeight - minHeight)) + .clamp(0.0, 1.0); + final leftPadding = 56 - (32 * expandRatio); + return FlexibleSpaceBar( + expandedTitleScale: 1.0, + titlePadding: EdgeInsets.only( + left: leftPadding, + bottom: 16, + ), + title: Text( + context.l10n.settingsApp, + style: TextStyle( + fontSize: 20 + (8 * expandRatio), + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ); + }, + ), + ), + + // ── Updates ──────────────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionApp), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.extension, + title: context.l10n.optionsExtensionStore, + subtitle: context.l10n.optionsExtensionStoreSubtitle, + value: settings.showExtensionStore, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setShowExtensionStore(v), + ), + SettingsSwitchItem( + icon: Icons.system_update, + title: context.l10n.optionsCheckUpdates, + subtitle: context.l10n.optionsCheckUpdatesSubtitle, + value: settings.checkForUpdates, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setCheckForUpdates(v), + showDivider: settings.checkForUpdates, + ), + if (settings.checkForUpdates) + _UpdateChannelSelector( + currentChannel: settings.updateChannel, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setUpdateChannel(v), + ), + ], + ), + ), + + // ── Data ─────────────────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionData), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.cleaning_services_outlined, + title: context.l10n.cleanupOrphanedDownloads, + subtitle: context.l10n.cleanupOrphanedDownloadsSubtitle, + onTap: () => _cleanupOrphanedDownloads(context, ref), + ), + SettingsItem( + icon: Icons.delete_forever, + title: context.l10n.optionsClearHistory, + subtitle: context.l10n.optionsClearHistorySubtitle, + onTap: () => + _showClearHistoryDialog(context, ref, colorScheme), + showDivider: false, + ), + ], + ), + ), + + // ── Debug ────────────────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionDebug), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.bug_report, + title: context.l10n.optionsDetailedLogging, + subtitle: settings.enableLogging + ? context.l10n.optionsDetailedLoggingOn + : context.l10n.optionsDetailedLoggingOff, + value: settings.enableLogging, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setEnableLogging(v), + showDivider: false, + ), + ], + ), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + ], + ), + ), + ); + } + + void _showClearHistoryDialog( + BuildContext context, + WidgetRef ref, + ColorScheme colorScheme, + ) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.dialogClearHistoryTitle), + content: Text(context.l10n.dialogClearHistoryMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.dialogCancel), + ), + TextButton( + onPressed: () { + ref.read(downloadHistoryProvider.notifier).clearHistory(); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarHistoryCleared)), + ); + }, + child: Text( + context.l10n.dialogClear, + style: TextStyle(color: colorScheme.error), + ), + ), + ], + ), + ); + } + + Future _cleanupOrphanedDownloads( + BuildContext context, + WidgetRef ref, + ) async { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + content: Row( + children: [ + const CircularProgressIndicator(), + const SizedBox(width: 16), + Text(context.l10n.cleanupOrphanedDownloads), + ], + ), + ), + ); + try { + final removed = await ref + .read(downloadHistoryProvider.notifier) + .cleanupOrphanedDownloads(); + if (context.mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + removed > 0 + ? context.l10n.cleanupOrphanedDownloadsResult(removed) + : context.l10n.cleanupOrphanedDownloadsNone, + ), + ), + ); + } + } catch (e) { + if (context.mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); + } + } + } +} + +class _UpdateChannelSelector extends StatelessWidget { + final String currentChannel; + final ValueChanged onChanged; + const _UpdateChannelSelector({ + required this.currentChannel, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) + : colorScheme.surfaceContainerHigh; + + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.new_releases, + color: colorScheme.onSurfaceVariant, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.optionsUpdateChannel, + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 2), + Text( + currentChannel == 'preview' + ? context.l10n.optionsUpdateChannelPreview + : context.l10n.optionsUpdateChannelStable, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + _ChannelChip( + label: context.l10n.channelStable, + isSelected: currentChannel == 'stable', + onTap: () => onChanged('stable'), + ), + const SizedBox(width: 8), + _ChannelChip( + label: context.l10n.channelPreview, + isSelected: currentChannel == 'preview', + onTap: () => onChanged('preview'), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + context.l10n.optionsUpdateChannelWarning, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ], + ), + ); + } +} + +class _ChannelChip extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + const _ChannelChip({ + required this.label, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) + : colorScheme.surfaceContainerHigh; + return Expanded( + child: 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), + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 543dbc1f..2a2de59f 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -1,18 +1,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; -import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/app_bar_layout.dart'; -import 'package:spotiflac_android/utils/file_access.dart'; -import 'package:spotiflac_android/screens/settings/lyrics_provider_priority_page.dart'; +import 'package:spotiflac_android/screens/settings/download_fallback_extensions_page.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; class DownloadSettingsPage extends ConsumerStatefulWidget { @@ -25,278 +20,12 @@ class DownloadSettingsPage extends ConsumerStatefulWidget { class _DownloadSettingsPageState extends ConsumerState { static const _builtInServices = ['tidal', 'qobuz']; - static const _songLinkRegions = [ - 'AD', - 'AE', - 'AG', - 'AL', - 'AM', - 'AO', - 'AR', - 'AT', - 'AU', - 'AZ', - 'BA', - 'BB', - 'BD', - 'BE', - 'BF', - 'BG', - 'BH', - 'BI', - 'BJ', - 'BN', - 'BO', - 'BR', - 'BS', - 'BT', - 'BW', - 'BZ', - 'CA', - 'CD', - 'CG', - 'CH', - 'CI', - 'CL', - 'CM', - 'CO', - 'CR', - 'CV', - 'CW', - 'CY', - 'CZ', - 'DE', - 'DJ', - 'DK', - 'DM', - 'DO', - 'DZ', - 'EC', - 'EE', - 'EG', - 'ES', - 'ET', - 'FI', - 'FJ', - 'FM', - 'FR', - 'GA', - 'GB', - 'GD', - 'GE', - 'GH', - 'GM', - 'GN', - 'GQ', - 'GR', - 'GT', - 'GW', - 'GY', - 'HK', - 'HN', - 'HR', - 'HT', - 'HU', - 'ID', - 'IE', - 'IL', - 'IN', - 'IQ', - 'IS', - 'IT', - 'JM', - 'JO', - 'JP', - 'KE', - 'KG', - 'KH', - 'KI', - 'KM', - 'KN', - 'KR', - 'KW', - 'KZ', - 'LA', - 'LB', - 'LC', - 'LI', - 'LK', - 'LR', - 'LS', - 'LT', - 'LU', - 'LV', - 'LY', - 'MA', - 'MC', - 'MD', - 'ME', - 'MG', - 'MH', - 'MK', - 'ML', - 'MN', - 'MO', - 'MR', - 'MT', - 'MU', - 'MV', - 'MW', - 'MX', - 'MY', - 'MZ', - 'NA', - 'NE', - 'NG', - 'NI', - 'NL', - 'NO', - 'NP', - 'NR', - 'NZ', - 'OM', - 'PA', - 'PE', - 'PG', - 'PH', - 'PK', - 'PL', - 'PS', - 'PT', - 'PW', - 'PY', - 'QA', - 'RO', - 'RS', - 'RW', - 'SA', - 'SB', - 'SC', - 'SE', - 'SG', - 'SI', - 'SK', - 'SL', - 'SM', - 'SN', - 'SR', - 'ST', - 'SV', - 'SZ', - 'TD', - 'TG', - 'TH', - 'TJ', - 'TL', - 'TN', - 'TO', - 'TR', - 'TT', - 'TV', - 'TW', - 'TZ', - 'UA', - 'UG', - 'US', - 'UY', - 'UZ', - 'VC', - 'VE', - 'VN', - 'VU', - 'WS', - 'XK', - 'ZA', - 'ZM', - 'ZW', - ]; - static const _songLinkRegionNames = { - 'US': 'United States', - 'GB': 'United Kingdom', - 'FR': 'France', - 'DE': 'Germany', - 'JP': 'Japan', - 'KR': 'South Korea', - 'IN': 'India', - 'ID': 'Indonesia', - 'BR': 'Brazil', - 'MX': 'Mexico', - 'AU': 'Australia', - 'CA': 'Canada', - 'XK': 'Kosovo', - }; - int _androidSdkVersion = 0; - bool _hasAllFilesAccess = false; - bool _artistFolderFiltersExpanded = false; - - @override - void initState() { - super.initState(); - _initDeviceInfo(); - } - - Future _initDeviceInfo() async { - if (Platform.isAndroid) { - final deviceInfo = DeviceInfoPlugin(); - final androidInfo = await deviceInfo.androidInfo; - final sdkVersion = androidInfo.version.sdkInt; - final hasAccess = await Permission.manageExternalStorage.isGranted; - if (mounted) { - setState(() { - _androidSdkVersion = sdkVersion; - _hasAllFilesAccess = hasAccess; - }); - } - } - } - - Future _requestAllFilesAccess() async { - final status = await Permission.manageExternalStorage.request(); - if (status.isGranted) { - ref.read(settingsProvider.notifier).setUseAllFilesAccess(true); - if (mounted) { - setState(() => _hasAllFilesAccess = true); - } - } else if (status.isPermanentlyDenied) { - if (mounted) { - final shouldOpen = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.l10n.setupStorageAccessRequired), - content: Text(context.l10n.allFilesAccessDeniedMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text(context.l10n.dialogCancel), - ), - FilledButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.setupOpenSettings), - ), - ], - ), - ); - if (shouldOpen == true) { - await openAppSettings(); - } - } - } - } - - Future _disableAllFilesAccess() async { - ref.read(settingsProvider.notifier).setUseAllFilesAccess(false); - // Note: We can't revoke the permission programmatically, - // but we can stop using it in the app - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.allFilesAccessDisabledMessage)), - ); - } - } @override Widget build(BuildContext context) { final settings = ref.watch(settingsProvider); + final extensionState = ref.watch(extensionProvider); + final hasExtensions = extensionState.extensions.isNotEmpty; final colorScheme = Theme.of(context).colorScheme; final topPadding = normalizedHeaderTopPadding(context); @@ -336,7 +65,7 @@ class _DownloadSettingsPageState extends ConsumerState { bottom: 16, ), title: Text( - context.l10n.downloadTitle, + context.l10n.settingsDownload, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, @@ -348,6 +77,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), ), + // ── Service ──────────────────────────────────────────────── SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionService), ), @@ -364,6 +94,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), ), + // ── Audio Quality ────────────────────────────────────────── SliverToBoxAdapter( child: SettingsSectionHeader( title: context.l10n.sectionAudioQuality, @@ -411,7 +142,6 @@ class _DownloadSettingsPageState extends ConsumerState { .setAudioQuality('HI_RES_LOSSLESS'), showDivider: isTidalService, ), - // Lossy 320kbps option (Tidal only) - downloads M4A AAC from server, converts to MP3/Opus if (isTidalService) _QualityOption( title: context.l10n.downloadLossy320, @@ -441,7 +171,7 @@ class _DownloadSettingsPageState extends ConsumerState { showDivider: false, ), ], - if (!isBuiltInService) ...[ + if (!isBuiltInService) Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Row( @@ -464,278 +194,25 @@ class _DownloadSettingsPageState extends ConsumerState { ], ), ), - ], - ], - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader(title: context.l10n.sectionLyrics), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.subtitles_outlined, - title: context.l10n.optionsEmbedLyrics, - subtitle: settings.embedMetadata - ? context.l10n.optionsEmbedLyricsSubtitle - : context.l10n.downloadEmbedLyricsDisabled, - value: settings.embedLyrics, - enabled: settings.embedMetadata, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setEmbedLyrics(value), - showDivider: settings.embedMetadata && settings.embedLyrics, - ), - if (settings.embedMetadata && settings.embedLyrics) ...[ - SettingsItem( - icon: Icons.lyrics_outlined, - title: context.l10n.lyricsMode, - subtitle: _getLyricsModeLabel( - context, - settings.lyricsMode, - ), - onTap: () => _showLyricsModePicker( - context, - ref, - settings.lyricsMode, - ), - ), - SettingsItem( - icon: Icons.source_outlined, - title: context.l10n.lyricsProvidersTitle, - subtitle: _getLyricsProvidersSubtitle( - settings.lyricsProviders, - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const LyricsProviderPriorityPage(), - ), - ), - ), - SettingsSwitchItem( - icon: Icons.translate_outlined, - title: context.l10n.downloadNeteaseIncludeTranslation, - subtitle: settings.lyricsIncludeTranslationNetease - ? context - .l10n - .downloadNeteaseIncludeTranslationEnabled - : context - .l10n - .downloadNeteaseIncludeTranslationDisabled, - value: settings.lyricsIncludeTranslationNetease, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setLyricsIncludeTranslationNetease(value), - ), - SettingsSwitchItem( - icon: Icons.text_fields_outlined, - title: context.l10n.downloadNeteaseIncludeRomanization, - subtitle: settings.lyricsIncludeRomanizationNetease - ? context - .l10n - .downloadNeteaseIncludeRomanizationEnabled - : context - .l10n - .downloadNeteaseIncludeRomanizationDisabled, - value: settings.lyricsIncludeRomanizationNetease, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setLyricsIncludeRomanizationNetease(value), - ), - SettingsSwitchItem( - icon: Icons.record_voice_over_outlined, - title: context.l10n.downloadAppleQqMultiPerson, - subtitle: settings.lyricsMultiPersonWordByWord - ? context.l10n.downloadAppleQqMultiPersonEnabled - : context.l10n.downloadAppleQqMultiPersonDisabled, - value: settings.lyricsMultiPersonWordByWord, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setLyricsMultiPersonWordByWord(value), - ), - SettingsItem( - icon: Icons.language_outlined, - title: context.l10n.downloadMusixmatchLanguage, - subtitle: settings.musixmatchLanguage.isEmpty - ? context.l10n.downloadMusixmatchLanguageAuto - : settings.musixmatchLanguage.toUpperCase(), - onTap: () => _showMusixmatchLanguagePicker( - context, - ref, - settings.musixmatchLanguage, - ), - showDivider: false, - ), - ], ], ), ), + // ── Network & Performance ────────────────────────────────── SliverToBoxAdapter( child: SettingsSectionHeader( - title: context.l10n.sectionFileSettings, + title: context.l10n.sectionPerformance, ), ), SliverToBoxAdapter( child: SettingsGroup( children: [ - SettingsItem( - icon: Icons.text_fields, - title: context.l10n.downloadFilenameFormat, - subtitle: settings.filenameFormat, - onTap: () => _showFormatEditor( - context, - ref, - settings.filenameFormat, - ), - ), - SettingsItem( - icon: Icons.music_note_outlined, - title: context.l10n.downloadSingleFilenameFormat, - subtitle: settings.singleFilenameFormat, - onTap: () => _showFormatEditor( - context, - ref, - settings.singleFilenameFormat, - onSave: ref - .read(settingsProvider.notifier) - .setSingleFilenameFormat, - title: context.l10n.downloadSingleFilenameFormat, - description: - context.l10n.downloadSingleFilenameFormatDescription, - ), - ), - SettingsItem( - icon: Icons.folder_outlined, - title: context.l10n.downloadDirectory, - subtitle: settings.downloadDirectory.isEmpty - ? (Platform.isIOS - ? context.l10n.setupAppDocumentsFolder - : 'Music/SpotiFLAC') - : settings.downloadDirectory, - onTap: () => _pickDirectory(context, ref), - ), - SettingsSwitchItem( - icon: Icons.library_music_outlined, - title: context.l10n.downloadSeparateSinglesFolder, - subtitle: settings.separateSingles - ? context.l10n.downloadSeparateSinglesEnabled - : context.l10n.downloadSeparateSinglesDisabled, - value: settings.separateSingles, - onChanged: (value) => ref + _ConcurrentDownloadsItem( + currentValue: settings.concurrentDownloads, + onChanged: (v) => ref .read(settingsProvider.notifier) - .setSeparateSingles(value), + .setConcurrentDownloads(v), ), - if (settings.separateSingles) - SettingsItem( - icon: Icons.folder_outlined, - title: context.l10n.downloadAlbumFolderStructure, - subtitle: _getAlbumFolderStructureLabel( - settings.albumFolderStructure, - ), - onTap: () => _showAlbumFolderStructurePicker( - context, - ref, - settings.albumFolderStructure, - ), - ), - if (!settings.separateSingles) - SettingsItem( - icon: Icons.create_new_folder_outlined, - title: context.l10n.downloadFolderOrganization, - subtitle: _getFolderOrganizationLabel( - settings.folderOrganization, - ), - onTap: () => _showFolderOrganizationPicker( - context, - ref, - settings.folderOrganization, - ), - ), - SettingsSwitchItem( - icon: Icons.playlist_play_outlined, - title: context.l10n.downloadCreatePlaylistSourceFolder, - subtitle: _getPlaylistFolderSubtitle(settings), - value: settings.createPlaylistFolder, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setCreatePlaylistFolder(value), - ), - SettingsSwitchItem( - icon: Icons.person_search_outlined, - title: context.l10n.downloadUseAlbumArtistForFolders, - subtitle: settings.useAlbumArtistForFolders - ? context - .l10n - .downloadUseAlbumArtistForFoldersAlbumSubtitle - : context - .l10n - .downloadUseAlbumArtistForFoldersTrackSubtitle, - value: settings.useAlbumArtistForFolders, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setUseAlbumArtistForFolders(value), - ), - SettingsItem( - icon: Icons.filter_alt_outlined, - title: context.l10n.downloadArtistNameFilters, - subtitle: _getArtistFolderFilterSubtitle( - context, - usePrimaryArtistOnly: settings.usePrimaryArtistOnly, - filterAlbumArtistContributors: - settings.filterContributingArtistsInAlbumArtist, - ), - trailing: Icon( - _artistFolderFiltersExpanded - ? Icons.expand_less - : Icons.expand_more, - ), - onTap: () { - setState(() { - _artistFolderFiltersExpanded = - !_artistFolderFiltersExpanded; - }); - }, - showDivider: !_artistFolderFiltersExpanded, - ), - if (_artistFolderFiltersExpanded) - SettingsSwitchItem( - icon: Icons.person_outline, - title: context.l10n.downloadUsePrimaryArtistOnly, - subtitle: settings.usePrimaryArtistOnly - ? context.l10n.downloadUsePrimaryArtistOnlyEnabled - : context.l10n.downloadUsePrimaryArtistOnlyDisabled, - value: settings.usePrimaryArtistOnly, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setUsePrimaryArtistOnly(value), - ), - if (_artistFolderFiltersExpanded) - SettingsSwitchItem( - icon: Icons.group_remove_outlined, - title: context.l10n.downloadFilterContributing, - subtitle: settings.filterContributingArtistsInAlbumArtist - ? context.l10n.downloadFilterContributingEnabled - : context.l10n.downloadFilterContributingDisabled, - value: settings.filterContributingArtistsInAlbumArtist, - onChanged: (value) => ref - .read(settingsProvider.notifier) - .setFilterContributingArtistsInAlbumArtist(value), - showDivider: false, - ), - ], - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader(title: context.l10n.sectionDownload), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ SettingsItem( icon: Icons.wifi, title: context.l10n.settingsDownloadNetwork, @@ -748,6 +225,77 @@ class _DownloadSettingsPageState extends ConsumerState { settings.downloadNetworkMode, ), ), + SettingsSwitchItem( + icon: Icons.security_outlined, + title: context.l10n.downloadNetworkCompatibilityMode, + subtitle: settings.networkCompatibilityMode + ? context.l10n.downloadNetworkCompatibilityModeEnabled + : context.l10n.downloadNetworkCompatibilityModeDisabled, + value: settings.networkCompatibilityMode, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setNetworkCompatibilityMode(value), + showDivider: false, + ), + ], + ), + ), + + // ── Fallback & Search ────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.sectionSearchSource, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + const _MetadataSourceSelector(), + const _DefaultSearchTabSelector(), + SettingsSwitchItem( + icon: Icons.sync, + title: context.l10n.optionsAutoFallback, + subtitle: context.l10n.optionsAutoFallbackSubtitle, + value: settings.autoFallback, + 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, + subtitle: context.l10n.downloadFallbackExtensionsSubtitle, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const DownloadFallbackExtensionsPage(), + ), + ), + showDivider: false, + ), + ], + ), + ), + + // ── Misc ─────────────────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionDownload), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ SettingsItem( icon: Icons.public, title: context.l10n.downloadSongLinkRegion, @@ -758,88 +306,20 @@ class _DownloadSettingsPageState extends ConsumerState { settings.songLinkRegion, ), ), - SettingsSwitchItem( - icon: Icons.security_outlined, - title: context.l10n.downloadNetworkCompatibilityMode, - subtitle: settings.networkCompatibilityMode - ? context.l10n.downloadNetworkCompatibilityModeEnabled - : context.l10n.downloadNetworkCompatibilityModeDisabled, - value: settings.networkCompatibilityMode, - onChanged: (value) { - ref - .read(settingsProvider.notifier) - .setNetworkCompatibilityMode(value); - }, - ), SettingsSwitchItem( icon: Icons.file_download_outlined, title: context.l10n.settingsAutoExportFailed, subtitle: context.l10n.settingsAutoExportFailedSubtitle, value: settings.autoExportFailedDownloads, - onChanged: (value) { - ref - .read(settingsProvider.notifier) - .setAutoExportFailedDownloads(value); - }, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setAutoExportFailedDownloads(value), showDivider: false, ), ], ), ), - if (Platform.isAndroid && _androidSdkVersion >= 33) ...[ - SliverToBoxAdapter( - child: SettingsSectionHeader( - title: context.l10n.sectionStorageAccess, - ), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.folder_special_outlined, - title: context.l10n.allFilesAccess, - subtitle: _hasAllFilesAccess - ? context.l10n.allFilesAccessEnabledSubtitle - : context.l10n.allFilesAccessDisabledSubtitle, - value: _hasAllFilesAccess && settings.useAllFilesAccess, - onChanged: (value) { - if (value) { - _requestAllFilesAccess(); - } else { - _disableAllFilesAccess(); - } - }, - showDivider: false, - ), - ], - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.info_outline, - size: 16, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - context.l10n.allFilesAccessDescription, - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: colorScheme.onSurfaceVariant), - ), - ), - ], - ), - ), - ), - ], - const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), @@ -847,765 +327,6 @@ class _DownloadSettingsPageState extends ConsumerState { ); } - String _getAlbumFolderStructureLabel(String structure) { - switch (structure) { - case 'album_only': - return 'Albums/Album Name/'; - case 'artist_year_album': - return 'Albums/Artist/[Year] Album/'; - case 'year_album': - return 'Albums/[Year] Album/'; - case 'artist_album_singles': - return 'Artist/Album/ + Artist/Singles/'; - case 'artist_album_flat': - return 'Artist/Album/ + Artist/song.flac'; - default: - return 'Albums/Artist/Album Name/'; - } - } - - void _showAlbumFolderStructurePicker( - BuildContext context, - WidgetRef ref, - String current, - ) { - showModalBottomSheet( - context: context, - useRootNavigator: true, - builder: (context) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.folder_outlined), - title: Text(context.l10n.albumFolderArtistAlbum), - subtitle: Text(context.l10n.albumFolderArtistAlbumSubtitle), - trailing: current == 'artist_album' - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setAlbumFolderStructure('artist_album'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.calendar_today_outlined), - title: Text(context.l10n.albumFolderArtistYearAlbum), - subtitle: Text(context.l10n.albumFolderArtistYearAlbumSubtitle), - trailing: current == 'artist_year_album' - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setAlbumFolderStructure('artist_year_album'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.album_outlined), - title: Text(context.l10n.albumFolderAlbumOnly), - subtitle: Text(context.l10n.albumFolderAlbumOnlySubtitle), - trailing: current == 'album_only' - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setAlbumFolderStructure('album_only'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.event_outlined), - title: Text(context.l10n.albumFolderYearAlbum), - subtitle: Text(context.l10n.albumFolderYearAlbumSubtitle), - trailing: current == 'year_album' - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setAlbumFolderStructure('year_album'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.person_outlined), - title: Text(context.l10n.albumFolderArtistAlbumSingles), - subtitle: Text( - context.l10n.albumFolderArtistAlbumSinglesSubtitle, - ), - trailing: current == 'artist_album_singles' - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setAlbumFolderStructure('artist_album_singles'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.person_outline_outlined), - title: Text(context.l10n.albumFolderArtistAlbumFlat), - subtitle: Text(context.l10n.albumFolderArtistAlbumFlatSubtitle), - trailing: current == 'artist_album_flat' - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setAlbumFolderStructure('artist_album_flat'); - Navigator.pop(context); - }, - ), - ], - ), - ), - ); - } - - void _showFormatEditor( - BuildContext context, - WidgetRef ref, - String current, { - void Function(String)? onSave, - String? title, - String? description, - }) { - final controller = TextEditingController(text: current); - final colorScheme = Theme.of(context).colorScheme; - - final basicTags = [ - '{artist}', - '{title}', - '{album}', - '{track}', - '{year}', - '{date}', - '{disc}', - ]; - final advancedTags = [ - '{track_raw}', - '{track:02}', - '{track:1}', - '{date:%Y}', - '{date:%Y-%m-%d}', - '{disc_raw}', - '{disc:02}', - ]; - var showAdvancedTags = RegExp( - r'\{(?:track_raw|disc_raw|track:\d+|disc:\d+|date:[^}]+)\}', - caseSensitive: false, - ).hasMatch(current); - - void insertTag(String tag) { - final text = controller.text; - final selection = controller.selection; - final start = selection.start >= 0 ? selection.start : text.length; - final end = selection.end >= 0 ? selection.end : text.length; - - String insertion = tag; - if (start > 0) { - final before = text.substring(0, start); - if (!before.trim().endsWith('-')) { - insertion = ' - $tag'; - } else if (before.trim().endsWith('-') && !before.endsWith(' ')) { - insertion = ' $tag'; - } - } - - final newText = text.replaceRange(start, end, insertion); - controller.value = TextEditingValue( - text: newText, - selection: TextSelection.collapsed(offset: start + insertion.length), - ); - } - - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: colorScheme.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - builder: (context) => StatefulBuilder( - builder: (context, setModalState) => Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: SingleChildScrollView( - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - width: 32, - height: 4, - margin: const EdgeInsets.only(bottom: 24), - decoration: BoxDecoration( - color: colorScheme.outlineVariant, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Text( - title ?? context.l10n.filenameFormat, - style: Theme.of(context).textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - description ?? context.l10n.downloadFilenameDescription, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - - TextField( - controller: controller, - decoration: InputDecoration( - hintText: '{artist} - {title}', - filled: true, - fillColor: colorScheme.surfaceContainerHighest - .withValues(alpha: 0.3), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, - ), - ), - autofocus: true, - ), - const SizedBox(height: 24), - - Text( - context.l10n.downloadFilenameInsertTag, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: basicTags.map((tag) { - return ActionChip( - label: Text(tag), - onPressed: () => insertTag(tag), - backgroundColor: colorScheme.surfaceContainerHighest - .withValues(alpha: 0.5), - side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - labelStyle: TextStyle( - color: colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ); - }).toList(), - ), - const SizedBox(height: 12), - SwitchListTile( - value: showAdvancedTags, - onChanged: (value) => - setModalState(() => showAdvancedTags = value), - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.filenameShowAdvancedTags), - subtitle: Text( - context.l10n.filenameShowAdvancedTagsDescription, - ), - ), - if (showAdvancedTags) ...[ - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: advancedTags.map((tag) { - return ActionChip( - label: Text(tag), - onPressed: () => insertTag(tag), - backgroundColor: colorScheme.surfaceContainerHighest - .withValues(alpha: 0.5), - side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - labelStyle: TextStyle( - color: colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ); - }).toList(), - ), - ], - - const SizedBox(height: 32), - - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text(context.l10n.dialogCancel), - ), - ), - const SizedBox(width: 12), - Expanded( - flex: 2, - child: FilledButton( - onPressed: () { - final save = - onSave ?? - ref - .read(settingsProvider.notifier) - .setFilenameFormat; - save(controller.text); - Navigator.pop(context); - }, - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text(context.l10n.dialogSave), - ), - ), - ], - ), - const SizedBox(height: 8), - ], - ), - ), - ), - ), - ), - ), - ); - } - - Future _pickDirectory(BuildContext context, WidgetRef ref) async { - if (Platform.isIOS) { - _showIOSDirectoryOptions(context, ref); - } else if (Platform.isAndroid) { - _showAndroidDirectoryOptions(context, ref); - } - } - - Future _getDefaultAndroidDirectory() async { - final directMusicPath = '/storage/emulated/0/Music/SpotiFLAC'; - try { - final musicDir = Directory(directMusicPath); - if (!await musicDir.exists()) { - await musicDir.create(recursive: true); - } - return musicDir.path; - } catch (_) {} - - try { - final externalDir = await getExternalStorageDirectory(); - if (externalDir != null) { - final musicDir = Directory( - '${externalDir.parent.parent.parent.parent.path}/Music/SpotiFLAC', - ); - if (!await musicDir.exists()) { - await musicDir.create(recursive: true); - } - return musicDir.path; - } - } catch (_) {} - - final appDir = await getApplicationDocumentsDirectory(); - final fallbackDir = Directory('${appDir.path}/SpotiFLAC'); - if (!await fallbackDir.exists()) { - await fallbackDir.create(recursive: true); - } - return fallbackDir.path; - } - - void _showAndroidDirectoryOptions(BuildContext context, WidgetRef ref) { - final colorScheme = Theme.of(context).colorScheme; - final settings = ref.read(settingsProvider); - final isSafMode = - settings.storageMode == 'saf' && settings.downloadTreeUri.isNotEmpty; - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - builder: (ctx) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.setupDownloadLocationTitle, - style: Theme.of( - ctx, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.downloadLocationSubtitle, - style: Theme.of(ctx).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ListTile( - leading: Icon(Icons.folder_special, color: colorScheme.primary), - title: Text(context.l10n.storageModeAppFolder), - subtitle: Text(context.l10n.storageModeAppFolderSubtitle), - trailing: !isSafMode ? const Icon(Icons.check) : null, - onTap: () async { - Navigator.pop(ctx); - final defaultDir = await _getDefaultAndroidDirectory(); - final notifier = ref.read(settingsProvider.notifier); - notifier.setStorageMode('app'); - notifier.setDownloadDirectory(defaultDir); - notifier.setDownloadTreeUri(''); - }, - ), - ListTile( - leading: Icon(Icons.folder_open, color: colorScheme.primary), - title: Text(context.l10n.storageModeSaf), - subtitle: Text(context.l10n.storageModeSafSubtitle), - trailing: isSafMode ? const Icon(Icons.check) : null, - onTap: () async { - Navigator.pop(ctx); - final result = await PlatformBridge.pickSafTree(); - if (result != null) { - final treeUri = result['tree_uri'] as String? ?? ''; - final displayName = result['display_name'] as String? ?? ''; - if (treeUri.isNotEmpty) { - ref.read(settingsProvider.notifier).setStorageMode('saf'); - ref - .read(settingsProvider.notifier) - .setDownloadTreeUri( - treeUri, - displayName: displayName.isNotEmpty - ? displayName - : treeUri, - ); - } - } - }, - ), - const SizedBox(height: 8), - ], - ), - ), - ); - } - - void _showIOSDirectoryOptions(BuildContext context, WidgetRef ref) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - builder: (ctx) => SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.setupDownloadLocationTitle, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.setupDownloadLocationIosMessage, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ListTile( - leading: Icon(Icons.folder_special, color: colorScheme.primary), - title: Text(context.l10n.setupAppDocumentsFolder), - subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle), - trailing: Icon(Icons.check_circle, color: colorScheme.primary), - onTap: () async { - final dir = await getApplicationDocumentsDirectory(); - ref - .read(settingsProvider.notifier) - .setDownloadDirectory(dir.path); - if (ctx.mounted) Navigator.pop(ctx); - }, - ), - ListTile( - leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), - title: Text(context.l10n.setupChooseFromFiles), - subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), - onTap: () async { - Navigator.pop(ctx); - if (Platform.isIOS) { - await Future.delayed(const Duration(milliseconds: 250)); - } - - // Note: iOS requires folder to have at least one file to be selectable - String? result; - try { - result = await FilePicker.platform.getDirectoryPath(); - } catch (e) { - if (ctx.mounted) { - ScaffoldMessenger.of(ctx).showSnackBar( - SnackBar( - content: Text( - ctx.l10n.snackbarFolderPickerFailed(e.toString()), - ), - backgroundColor: Theme.of(ctx).colorScheme.error, - duration: const Duration(seconds: 4), - ), - ); - } - return; - } - - if (result != null) { - // iOS: Validate the selected path is writable (not iCloud or container root) - if (Platform.isIOS) { - final validation = validateIosPath(result); - if (!validation.isValid) { - if (ctx.mounted) { - ScaffoldMessenger.of(ctx).showSnackBar( - SnackBar( - content: Text( - validation.errorReason ?? - context.l10n.setupIcloudNotSupported, - ), - backgroundColor: Theme.of(ctx).colorScheme.error, - duration: const Duration(seconds: 4), - ), - ); - } - return; - } - } - ref - .read(settingsProvider.notifier) - .setDownloadDirectory(result); - } - }, - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Icon( - Icons.info_outline, - size: 20, - color: colorScheme.tertiary, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - context.l10n.setupIosEmptyFolderWarning, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onTertiaryContainer, - ), - ), - ), - ], - ), - ), - ), - const SizedBox(height: 8), - ], - ), - ), - ); - } - - String _getFolderOrganizationLabel(String value) { - switch (value) { - case 'playlist': - return 'By Playlist'; - case 'artist': - return 'By Artist'; - case 'album': - return 'By Album'; - case 'artist_album': - return 'Artist/Album'; - default: - return 'None'; - } - } - - String _getPlaylistFolderSubtitle(AppSettings settings) { - if (settings.folderOrganization == 'playlist') { - return context.l10n.downloadCreatePlaylistSourceFolderRedundant; - } - if (settings.createPlaylistFolder) { - return context.l10n.downloadCreatePlaylistSourceFolderEnabled; - } - return context.l10n.downloadCreatePlaylistSourceFolderDisabled; - } - - String _getArtistFolderFilterSubtitle( - BuildContext context, { - required bool usePrimaryArtistOnly, - required bool filterAlbumArtistContributors, - }) { - final statuses = [ - usePrimaryArtistOnly ? 'Primary only: On' : 'Primary only: Off', - filterAlbumArtistContributors - ? 'Album Artist metadata: Primary only' - : 'Album Artist metadata: Full', - ]; - return statuses.join(' | '); - } - - String _getLyricsModeLabel(BuildContext context, String mode) { - switch (mode) { - case 'external': - return context.l10n.lyricsModeExternal; - case 'both': - return context.l10n.lyricsModeBoth; - default: - return context.l10n.lyricsModeEmbed; - } - } - - String _getSongLinkRegionLabel(String code) { - final normalized = code.trim().toUpperCase(); - final effective = normalized.isEmpty ? 'US' : normalized; - final name = _songLinkRegionNames[effective]; - if (name == null) return effective; - return '$effective - $name'; - } - - void _showLyricsModePicker( - BuildContext context, - WidgetRef ref, - String current, - ) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - 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.lyricsMode, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.lyricsModeDescription, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ListTile( - leading: const Icon(Icons.audiotrack), - title: Text(context.l10n.lyricsModeEmbed), - subtitle: Text(context.l10n.lyricsModeEmbedSubtitle), - trailing: current == 'embed' ? const Icon(Icons.check) : null, - onTap: () { - ref.read(settingsProvider.notifier).setLyricsMode('embed'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.insert_drive_file_outlined), - title: Text(context.l10n.lyricsModeExternal), - subtitle: Text(context.l10n.lyricsModeExternalSubtitle), - trailing: current == 'external' ? const Icon(Icons.check) : null, - onTap: () { - ref.read(settingsProvider.notifier).setLyricsMode('external'); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.library_music_outlined), - title: Text(context.l10n.lyricsModeBoth), - subtitle: Text(context.l10n.lyricsModeBothSubtitle), - trailing: current == 'both' ? const Icon(Icons.check) : null, - onTap: () { - ref.read(settingsProvider.notifier).setLyricsMode('both'); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - - static const _providerDisplayNames = { - 'lrclib': 'LRCLIB', - 'netease': 'Netease', - 'musixmatch': 'Musixmatch', - 'apple_music': 'Apple Music', - 'qqmusic': 'QQ Music', - }; - - String _getLyricsProvidersSubtitle(List providers) { - if (providers.isEmpty) return context.l10n.downloadProvidersNoneEnabled; - return providers.map((p) => _providerDisplayNames[p] ?? p).join(' > '); - } - - String _normalizeMusixmatchLanguage(String value) { - final normalized = value.trim().toLowerCase(); - return normalized.replaceAll(RegExp(r'[^a-z0-9\-_]'), ''); - } - String _getTidalHighFormatLabel(BuildContext context, String format) { switch (format) { case 'mp3_320': @@ -1619,6 +340,19 @@ class _DownloadSettingsPageState extends ConsumerState { } } + String _getSongLinkRegionLabel(String code) { + const names = { + 'US': 'United States', 'GB': 'United Kingdom', 'FR': 'France', + 'DE': 'Germany', 'JP': 'Japan', 'KR': 'South Korea', + 'IN': 'India', 'ID': 'Indonesia', 'BR': 'Brazil', + 'MX': 'Mexico', 'AU': 'Australia', 'CA': 'Canada', 'XK': 'Kosovo', + }; + final normalized = code.trim().toUpperCase(); + final effective = normalized.isEmpty ? 'US' : normalized; + final name = names[effective]; + return name == null ? effective : '$effective - $name'; + } + void _showTidalHighFormatPicker( BuildContext context, WidgetRef ref, @@ -1641,18 +375,16 @@ class _DownloadSettingsPageState extends ConsumerState { padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( context.l10n.downloadLossy320Format, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + 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, - ), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ListTile( @@ -1663,9 +395,7 @@ class _DownloadSettingsPageState extends ConsumerState { ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('mp3_320'); + ref.read(settingsProvider.notifier).setTidalHighFormat('mp3_320'); Navigator.pop(context); }, ), @@ -1677,9 +407,7 @@ class _DownloadSettingsPageState extends ConsumerState { ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_256'); + ref.read(settingsProvider.notifier).setTidalHighFormat('opus_256'); Navigator.pop(context); }, ), @@ -1691,9 +419,7 @@ class _DownloadSettingsPageState extends ConsumerState { ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () { - ref - .read(settingsProvider.notifier) - .setTidalHighFormat('opus_128'); + ref.read(settingsProvider.notifier).setTidalHighFormat('opus_128'); Navigator.pop(context); }, ), @@ -1704,94 +430,6 @@ class _DownloadSettingsPageState extends ConsumerState { ); } - void _showMusixmatchLanguagePicker( - BuildContext context, - WidgetRef ref, - String currentLanguage, - ) { - final colorScheme = Theme.of(context).colorScheme; - final controller = TextEditingController(text: currentLanguage); - - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - isScrollControlled: true, - builder: (context) => Padding( - padding: EdgeInsets.only( - left: 24, - right: 24, - top: 24, - bottom: 24 + MediaQuery.of(context).viewInsets.bottom, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.downloadMusixmatchLanguage, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - context.l10n.downloadMusixmatchLanguageDesc, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - TextField( - controller: controller, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - labelText: context.l10n.downloadMusixmatchLanguageCode, - hintText: context.l10n.downloadMusixmatchLanguageHint, - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.dialogCancel), - ), - const SizedBox(width: 8), - TextButton( - onPressed: () { - ref - .read(settingsProvider.notifier) - .setMusixmatchLanguage(''); - Navigator.pop(context); - }, - child: Text(context.l10n.downloadMusixmatchAuto), - ), - const SizedBox(width: 8), - FilledButton( - onPressed: () { - final normalized = _normalizeMusixmatchLanguage( - controller.text, - ); - ref - .read(settingsProvider.notifier) - .setMusixmatchLanguage(normalized); - Navigator.pop(context); - }, - child: Text(context.l10n.dialogSave), - ), - ], - ), - ], - ), - ), - ); - } - void _showNetworkModePicker( BuildContext context, WidgetRef ref, @@ -1814,18 +452,16 @@ class _DownloadSettingsPageState extends ConsumerState { padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( context.l10n.settingsDownloadNetwork, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( context.l10n.settingsDownloadNetworkSubtitle, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), ), ListTile( @@ -1836,9 +472,7 @@ class _DownloadSettingsPageState extends ConsumerState { ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () { - ref - .read(settingsProvider.notifier) - .setDownloadNetworkMode('any'); + ref.read(settingsProvider.notifier).setDownloadNetworkMode('any'); Navigator.pop(context); }, ), @@ -1868,6 +502,27 @@ class _DownloadSettingsPageState extends ConsumerState { WidgetRef ref, String current, ) { + const regions = [ + 'AD','AE','AG','AL','AM','AO','AR','AT','AU','AZ','BA','BB','BD','BE', + 'BF','BG','BH','BI','BJ','BN','BO','BR','BS','BT','BW','BZ','CA','CD', + 'CG','CH','CI','CL','CM','CO','CR','CV','CW','CY','CZ','DE','DJ','DK', + 'DM','DO','DZ','EC','EE','EG','ES','ET','FI','FJ','FM','FR','GA','GB', + 'GD','GE','GH','GM','GN','GQ','GR','GT','GW','GY','HK','HN','HR','HT', + 'HU','ID','IE','IL','IN','IQ','IS','IT','JM','JO','JP','KE','KG','KH', + 'KI','KM','KN','KR','KW','KZ','LA','LB','LC','LI','LK','LR','LS','LT', + 'LU','LV','LY','MA','MC','MD','ME','MG','MH','MK','ML','MN','MO','MR', + 'MT','MU','MV','MW','MX','MY','MZ','NA','NE','NG','NI','NL','NO','NP', + 'NR','NZ','OM','PA','PE','PG','PH','PK','PL','PS','PT','PW','PY','QA', + 'RO','RS','RW','SA','SB','SC','SE','SG','SI','SK','SL','SM','SN','SR', + 'ST','SV','SZ','TD','TG','TH','TJ','TL','TN','TO','TR','TT','TV','TW', + 'TZ','UA','UG','US','UY','UZ','VC','VE','VN','VU','WS','XK','ZA','ZM','ZW', + ]; + const names = { + 'US': 'United States', 'GB': 'United Kingdom', 'FR': 'France', + 'DE': 'Germany', 'JP': 'Japan', 'KR': 'South Korea', + 'IN': 'India', 'ID': 'Indonesia', 'BR': 'Brazil', + 'MX': 'Mexico', 'AU': 'Australia', 'CA': 'Canada', 'XK': 'Kosovo', + }; final colorScheme = Theme.of(context).colorScheme; final normalizedCurrent = current.trim().toUpperCase(); showModalBottomSheet( @@ -1888,30 +543,27 @@ class _DownloadSettingsPageState extends ConsumerState { padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Text( context.l10n.downloadSongLinkRegion, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), child: Text( context.l10n.downloadSongLinkRegionDesc, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), ), Expanded( child: ListView.builder( - itemCount: _songLinkRegions.length, + itemCount: regions.length, itemBuilder: (context, index) { - final code = _songLinkRegions[index]; + final code = regions[index]; final isSelected = code == normalizedCurrent; - final displayName = _songLinkRegionNames[code]; return ListTile( title: Text(code), - subtitle: displayName != null ? Text(displayName) : null, + subtitle: names[code] != null ? Text(names[code]!) : null, trailing: isSelected ? Icon(Icons.check, color: colorScheme.primary) : null, @@ -1931,117 +583,10 @@ class _DownloadSettingsPageState extends ConsumerState { ), ); } - - void _showFolderOrganizationPicker( - BuildContext context, - WidgetRef ref, - String current, - ) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - useRootNavigator: true, - backgroundColor: colorScheme.surfaceContainerHigh, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), - ), - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, - ), - builder: (context) => SafeArea( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), - child: Text( - context.l10n.downloadFolderOrganization, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.folderOrganizationDescription, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - _FolderOption( - title: context.l10n.folderOrganizationNone, - subtitle: context.l10n.folderOrganizationNoneSubtitle, - example: 'SpotiFLAC/Track.flac', - isSelected: current == 'none', - onTap: () { - ref - .read(settingsProvider.notifier) - .setFolderOrganization('none'); - Navigator.pop(context); - }, - ), - _FolderOption( - title: context.l10n.folderOrganizationByPlaylist, - subtitle: context.l10n.folderOrganizationByPlaylistSubtitle, - example: 'SpotiFLAC/Playlist Name/Track.flac', - isSelected: current == 'playlist', - onTap: () { - ref - .read(settingsProvider.notifier) - .setFolderOrganization('playlist'); - Navigator.pop(context); - }, - ), - _FolderOption( - title: context.l10n.folderOrganizationByArtist, - subtitle: context.l10n.folderOrganizationByArtistSubtitle, - example: 'SpotiFLAC/Artist Name/Track.flac', - isSelected: current == 'artist', - onTap: () { - ref - .read(settingsProvider.notifier) - .setFolderOrganization('artist'); - Navigator.pop(context); - }, - ), - _FolderOption( - title: context.l10n.folderOrganizationByAlbum, - subtitle: context.l10n.folderOrganizationByAlbumSubtitle, - example: 'SpotiFLAC/Album Name/Track.flac', - isSelected: current == 'album', - onTap: () { - ref - .read(settingsProvider.notifier) - .setFolderOrganization('album'); - Navigator.pop(context); - }, - ), - _FolderOption( - title: context.l10n.folderOrganizationByArtistAlbum, - subtitle: context.l10n.folderOrganizationByArtistAlbumSubtitle, - example: 'SpotiFLAC/Artist/Album/Track.flac', - isSelected: current == 'artist_album', - onTap: () { - ref - .read(settingsProvider.notifier) - .setFolderOrganization('artist_album'); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ); - } } +// ── Private widgets (reused from original) ───────────────────────────────── + class _ServiceSelector extends ConsumerWidget { final String currentService; final ValueChanged onChanged; @@ -2129,14 +674,12 @@ class _ServiceChip extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; 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), @@ -2234,16 +777,91 @@ class _QualityOption extends StatelessWidget { } } -class _FolderOption extends StatelessWidget { - final String title; - final String subtitle; - final String example; +class _ConcurrentDownloadsItem extends StatelessWidget { + final int currentValue; + final ValueChanged onChanged; + const _ConcurrentDownloadsItem({ + required this.currentValue, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.download_for_offline, + color: colorScheme.onSurfaceVariant, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.optionsConcurrentDownloads, + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 2), + Text( + currentValue == 1 + ? context.l10n.optionsConcurrentSequential + : context.l10n.optionsConcurrentParallel(currentValue), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + for (final n in [1, 2, 3, 4, 5]) ...[ + if (n > 1) const SizedBox(width: 8), + _ConcurrentChip( + label: '$n', + isSelected: currentValue == n, + onTap: () => onChanged(n), + ), + ], + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Icon(Icons.warning_amber_rounded, size: 16, color: colorScheme.error), + const SizedBox(width: 8), + Expanded( + child: Text( + context.l10n.optionsConcurrentWarning, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.error), + ), + ), + ], + ), + ], + ), + ); + } +} + +class _ConcurrentChip extends StatelessWidget { + final String label; final bool isSelected; final VoidCallback onTap; - const _FolderOption({ - required this.title, - required this.subtitle, - required this.example, + const _ConcurrentChip({ + required this.label, required this.isSelected, required this.onTap, }); @@ -2251,28 +869,204 @@ class _FolderOption extends StatelessWidget { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), - title: Text(title), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(subtitle), - const SizedBox(height: 4), - Text( - example, - style: TextStyle( - fontFamily: 'monospace', - fontSize: 11, - color: colorScheme.primary, + final isDark = Theme.of(context).brightness == Brightness.dark; + final unselectedColor = isDark + ? Color.alphaBlend( + Colors.white.withValues(alpha: 0.05), + colorScheme.surface, + ) + : colorScheme.surfaceContainerHigh; + return Expanded( + child: 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), + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ), + ), ), ), - ], + ), ), - trailing: isSelected - ? Icon(Icons.check_circle, color: colorScheme.primary) - : Icon(Icons.circle_outlined, color: colorScheme.outline), - onTap: onTap, + ); + } +} + +// Imported from options_settings_page — search source selectors +class _MetadataSourceSelector extends ConsumerWidget { + const _MetadataSourceSelector(); + + static const _builtInProviders = {'tidal': 'Tidal', 'qobuz': 'Qobuz'}; + + Extension? _defaultSearchExtension(List extensions) { + return extensions + .where( + (ext) => + ext.enabled && + ext.hasCustomSearch && + ext.searchBehavior?.primary == true, + ) + .firstOrNull ?? + extensions + .where((ext) => ext.enabled && ext.hasCustomSearch) + .firstOrNull; + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + final settings = ref.watch(settingsProvider); + 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'; + final defaultProviderLabel = + '${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)'; + final searchProvider = + isValidBuiltIn || + extState.extensions.any( + (e) => + e.enabled && e.hasCustomSearch && e.id == rawSearchProvider, + ) + ? rawSearchProvider + : ''; + final isBuiltIn = _builtInProviders.containsKey(searchProvider); + + Extension? activeExtension; + if (searchProvider.isNotEmpty && !isBuiltIn) { + activeExtension = extState.extensions + .where((e) => e.id == searchProvider && e.enabled) + .firstOrNull; + } + final hasNonDefaultProvider = isBuiltIn || activeExtension != null; + + String subtitle; + if (isBuiltIn) { + subtitle = 'Using ${_builtInProviders[searchProvider]}'; + } else if (activeExtension != null) { + subtitle = context.l10n.optionsUsingExtension(activeExtension.displayName); + } else { + subtitle = context.l10n.optionsPrimaryProviderSubtitle; + } + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.optionsPrimaryProvider, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: hasNonDefaultProvider + ? colorScheme.primary + : colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _SearchProviderChip( + 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( + label: ext.displayName, + isSelected: searchProvider == ext.id, + onTap: () => ref + .read(settingsProvider.notifier) + .setSearchProvider(ext.id), + ), + ], + ), + ], + ), + ); + } +} + +class _SearchProviderChip extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + const _SearchProviderChip({ + required this.label, + required this.isSelected, + required this.onTap, + }); + + @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, + ), + ); + } +} + +class _DefaultSearchTabSelector extends ConsumerWidget { + const _DefaultSearchTabSelector(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final colorScheme = Theme.of(context).colorScheme; + 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', + ); + }, ); } } diff --git a/lib/screens/settings/files_settings_page.dart b/lib/screens/settings/files_settings_page.dart new file mode 100644 index 00000000..0382abc4 --- /dev/null +++ b/lib/screens/settings/files_settings_page.dart @@ -0,0 +1,1055 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/models/settings.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/app_bar_layout.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; +import 'package:spotiflac_android/widgets/settings_group.dart'; + +class FilesSettingsPage extends ConsumerStatefulWidget { + const FilesSettingsPage({super.key}); + + @override + ConsumerState createState() => _FilesSettingsPageState(); +} + +class _FilesSettingsPageState extends ConsumerState { + int _androidSdkVersion = 0; + bool _hasAllFilesAccess = false; + bool _artistFolderFiltersExpanded = false; + + @override + void initState() { + super.initState(); + _initDeviceInfo(); + } + + Future _initDeviceInfo() async { + if (Platform.isAndroid) { + final deviceInfo = DeviceInfoPlugin(); + final androidInfo = await deviceInfo.androidInfo; + final sdkVersion = androidInfo.version.sdkInt; + final hasAccess = await Permission.manageExternalStorage.isGranted; + if (mounted) { + setState(() { + _androidSdkVersion = sdkVersion; + _hasAllFilesAccess = hasAccess; + }); + } + } + } + + Future _requestAllFilesAccess() async { + final status = await Permission.manageExternalStorage.request(); + if (status.isGranted) { + ref.read(settingsProvider.notifier).setUseAllFilesAccess(true); + if (mounted) setState(() => _hasAllFilesAccess = true); + } else if (status.isPermanentlyDenied) { + if (mounted) { + final shouldOpen = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.setupStorageAccessRequired), + content: Text(context.l10n.allFilesAccessDeniedMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(context.l10n.dialogCancel), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.setupOpenSettings), + ), + ], + ), + ); + if (shouldOpen == true) await openAppSettings(); + } + } + } + + Future _disableAllFilesAccess() async { + ref.read(settingsProvider.notifier).setUseAllFilesAccess(false); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.allFilesAccessDisabledMessage)), + ); + } + } + + @override + Widget build(BuildContext context) { + final settings = ref.watch(settingsProvider); + final colorScheme = Theme.of(context).colorScheme; + final topPadding = normalizedHeaderTopPadding(context); + + return PopScope( + canPop: true, + child: Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 120 + topPadding, + collapsedHeight: kToolbarHeight, + floating: false, + pinned: true, + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final maxHeight = 120 + topPadding; + final minHeight = kToolbarHeight + topPadding; + final expandRatio = + ((constraints.maxHeight - minHeight) / + (maxHeight - minHeight)) + .clamp(0.0, 1.0); + final leftPadding = 56 - (32 * expandRatio); + return FlexibleSpaceBar( + expandedTitleScale: 1.0, + titlePadding: EdgeInsets.only( + left: leftPadding, + bottom: 16, + ), + title: Text( + context.l10n.settingsFiles, + style: TextStyle( + fontSize: 20 + (8 * expandRatio), + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ); + }, + ), + ), + + // ── Download Location ────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.setupDownloadLocationTitle, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.folder_outlined, + title: context.l10n.downloadDirectory, + subtitle: settings.downloadDirectory.isEmpty + ? (Platform.isIOS + ? context.l10n.setupAppDocumentsFolder + : 'Music/SpotiFLAC') + : settings.downloadDirectory, + onTap: () => _pickDirectory(context, ref), + showDivider: false, + ), + ], + ), + ), + + // ── Filename Formats ─────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.sectionFileSettings, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.text_fields, + title: context.l10n.downloadFilenameFormat, + subtitle: settings.filenameFormat, + onTap: () => _showFormatEditor( + context, + ref, + settings.filenameFormat, + ), + ), + SettingsItem( + icon: Icons.music_note_outlined, + title: context.l10n.downloadSingleFilenameFormat, + subtitle: settings.singleFilenameFormat, + onTap: () => _showFormatEditor( + context, + ref, + settings.singleFilenameFormat, + onSave: ref + .read(settingsProvider.notifier) + .setSingleFilenameFormat, + title: context.l10n.downloadSingleFilenameFormat, + description: + context.l10n.downloadSingleFilenameFormatDescription, + ), + showDivider: false, + ), + ], + ), + ), + + // ── Folder Structure ─────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.downloadFolderOrganization, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.library_music_outlined, + title: context.l10n.downloadSeparateSinglesFolder, + subtitle: settings.separateSingles + ? context.l10n.downloadSeparateSinglesEnabled + : context.l10n.downloadSeparateSinglesDisabled, + value: settings.separateSingles, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setSeparateSingles(value), + ), + if (settings.separateSingles) + SettingsItem( + icon: Icons.folder_outlined, + title: context.l10n.downloadAlbumFolderStructure, + subtitle: _getAlbumFolderStructureLabel( + settings.albumFolderStructure, + ), + onTap: () => _showAlbumFolderStructurePicker( + context, + ref, + settings.albumFolderStructure, + ), + ), + if (!settings.separateSingles) + SettingsItem( + icon: Icons.create_new_folder_outlined, + title: context.l10n.downloadFolderOrganization, + subtitle: _getFolderOrganizationLabel( + settings.folderOrganization, + ), + onTap: () => _showFolderOrganizationPicker( + context, + ref, + settings.folderOrganization, + ), + ), + SettingsSwitchItem( + icon: Icons.playlist_play_outlined, + title: context.l10n.downloadCreatePlaylistSourceFolder, + subtitle: _getPlaylistFolderSubtitle(settings), + value: settings.createPlaylistFolder, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setCreatePlaylistFolder(value), + ), + SettingsSwitchItem( + icon: Icons.person_search_outlined, + title: context.l10n.downloadUseAlbumArtistForFolders, + subtitle: settings.useAlbumArtistForFolders + ? context + .l10n + .downloadUseAlbumArtistForFoldersAlbumSubtitle + : context + .l10n + .downloadUseAlbumArtistForFoldersTrackSubtitle, + value: settings.useAlbumArtistForFolders, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setUseAlbumArtistForFolders(value), + ), + SettingsItem( + icon: Icons.filter_alt_outlined, + title: context.l10n.downloadArtistNameFilters, + subtitle: _getArtistFolderFilterSubtitle( + context, + usePrimaryArtistOnly: settings.usePrimaryArtistOnly, + filterAlbumArtistContributors: + settings.filterContributingArtistsInAlbumArtist, + ), + trailing: Icon( + _artistFolderFiltersExpanded + ? Icons.expand_less + : Icons.expand_more, + ), + onTap: () => setState(() { + _artistFolderFiltersExpanded = + !_artistFolderFiltersExpanded; + }), + showDivider: !_artistFolderFiltersExpanded, + ), + if (_artistFolderFiltersExpanded) ...[ + SettingsSwitchItem( + icon: Icons.person_outline, + title: context.l10n.downloadUsePrimaryArtistOnly, + subtitle: settings.usePrimaryArtistOnly + ? context.l10n.downloadUsePrimaryArtistOnlyEnabled + : context.l10n.downloadUsePrimaryArtistOnlyDisabled, + value: settings.usePrimaryArtistOnly, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setUsePrimaryArtistOnly(value), + ), + SettingsSwitchItem( + icon: Icons.group_remove_outlined, + title: context.l10n.downloadFilterContributing, + subtitle: settings.filterContributingArtistsInAlbumArtist + ? context.l10n.downloadFilterContributingEnabled + : context.l10n.downloadFilterContributingDisabled, + value: settings.filterContributingArtistsInAlbumArtist, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setFilterContributingArtistsInAlbumArtist(value), + showDivider: false, + ), + ], + ], + ), + ), + + // ── Storage Access (Android 13+) ─────────────────────────── + if (Platform.isAndroid && _androidSdkVersion >= 33) ...[ + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.sectionStorageAccess, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.folder_special_outlined, + title: context.l10n.allFilesAccess, + subtitle: _hasAllFilesAccess + ? context.l10n.allFilesAccessEnabledSubtitle + : context.l10n.allFilesAccessDisabledSubtitle, + value: _hasAllFilesAccess && + settings.useAllFilesAccess, + onChanged: (value) { + if (value) { + _requestAllFilesAccess(); + } else { + _disableAllFilesAccess(); + } + }, + showDivider: false, + ), + ], + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + size: 16, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + context.l10n.allFilesAccessDescription, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + ], + ), + ), + ), + ], + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + ], + ), + ), + ); + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + String _getAlbumFolderStructureLabel(String structure) { + switch (structure) { + case 'album_only': + return 'Albums/Album Name/'; + case 'artist_year_album': + return 'Albums/Artist/[Year] Album/'; + case 'year_album': + return 'Albums/[Year] Album/'; + case 'artist_album_singles': + return 'Artist/Album/ + Artist/Singles/'; + case 'artist_album_flat': + return 'Artist/Album/ + Artist/song.flac'; + default: + return 'Albums/Artist/Album Name/'; + } + } + + String _getFolderOrganizationLabel(String value) { + switch (value) { + case 'playlist': + return 'By Playlist'; + case 'artist': + return 'By Artist'; + case 'album': + return 'By Album'; + case 'artist_album': + return 'Artist/Album'; + default: + return 'None'; + } + } + + String _getPlaylistFolderSubtitle(AppSettings settings) { + if (settings.folderOrganization == 'playlist') { + return context.l10n.downloadCreatePlaylistSourceFolderRedundant; + } + if (settings.createPlaylistFolder) { + return context.l10n.downloadCreatePlaylistSourceFolderEnabled; + } + return context.l10n.downloadCreatePlaylistSourceFolderDisabled; + } + + String _getArtistFolderFilterSubtitle( + BuildContext context, { + required bool usePrimaryArtistOnly, + required bool filterAlbumArtistContributors, + }) { + final statuses = [ + usePrimaryArtistOnly ? 'Primary only: On' : 'Primary only: Off', + filterAlbumArtistContributors + ? 'Album Artist metadata: Primary only' + : 'Album Artist metadata: Full', + ]; + return statuses.join(' | '); + } + + Future _pickDirectory(BuildContext context, WidgetRef ref) async { + if (Platform.isIOS) { + _showIOSDirectoryOptions(context, ref); + } else if (Platform.isAndroid) { + _showAndroidDirectoryOptions(context, ref); + } + } + + Future _getDefaultAndroidDirectory() async { + const directMusicPath = '/storage/emulated/0/Music/SpotiFLAC'; + try { + final musicDir = Directory(directMusicPath); + if (!await musicDir.exists()) await musicDir.create(recursive: true); + return musicDir.path; + } catch (_) {} + try { + final externalDir = await getExternalStorageDirectory(); + if (externalDir != null) { + final musicDir = Directory( + '${externalDir.parent.parent.parent.parent.path}/Music/SpotiFLAC', + ); + if (!await musicDir.exists()) await musicDir.create(recursive: true); + return musicDir.path; + } + } catch (_) {} + final appDir = await getApplicationDocumentsDirectory(); + final fallbackDir = Directory('${appDir.path}/SpotiFLAC'); + if (!await fallbackDir.exists()) await fallbackDir.create(recursive: true); + return fallbackDir.path; + } + + void _showAndroidDirectoryOptions(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + final settings = ref.read(settingsProvider); + final isSafMode = + settings.storageMode == 'saf' && settings.downloadTreeUri.isNotEmpty; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.setupDownloadLocationTitle, + style: Theme.of(ctx).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.downloadLocationSubtitle, + style: Theme.of(ctx).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + ListTile( + leading: Icon(Icons.folder_special, color: colorScheme.primary), + title: Text(context.l10n.storageModeAppFolder), + subtitle: Text(context.l10n.storageModeAppFolderSubtitle), + trailing: !isSafMode ? const Icon(Icons.check) : null, + onTap: () async { + Navigator.pop(ctx); + final defaultDir = await _getDefaultAndroidDirectory(); + final notifier = ref.read(settingsProvider.notifier); + notifier.setStorageMode('app'); + notifier.setDownloadDirectory(defaultDir); + notifier.setDownloadTreeUri(''); + }, + ), + ListTile( + leading: Icon(Icons.folder_open, color: colorScheme.primary), + title: Text(context.l10n.storageModeSaf), + subtitle: Text(context.l10n.storageModeSafSubtitle), + trailing: isSafMode ? const Icon(Icons.check) : null, + onTap: () async { + Navigator.pop(ctx); + final result = await PlatformBridge.pickSafTree(); + if (result != null) { + final treeUri = result['tree_uri'] as String? ?? ''; + final displayName = result['display_name'] as String? ?? ''; + if (treeUri.isNotEmpty) { + ref.read(settingsProvider.notifier).setStorageMode('saf'); + ref.read(settingsProvider.notifier).setDownloadTreeUri( + treeUri, + displayName: displayName.isNotEmpty ? displayName : treeUri, + ); + } + } + }, + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } + + void _showIOSDirectoryOptions(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.setupDownloadLocationTitle, + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.setupDownloadLocationIosMessage, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + ListTile( + leading: Icon(Icons.folder_special, color: colorScheme.primary), + title: Text(context.l10n.setupAppDocumentsFolder), + subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle), + trailing: Icon(Icons.check_circle, color: colorScheme.primary), + onTap: () async { + final dir = await getApplicationDocumentsDirectory(); + ref + .read(settingsProvider.notifier) + .setDownloadDirectory(dir.path); + if (ctx.mounted) Navigator.pop(ctx); + }, + ), + ListTile( + leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant), + title: Text(context.l10n.setupChooseFromFiles), + subtitle: Text(context.l10n.setupChooseFromFilesSubtitle), + onTap: () async { + Navigator.pop(ctx); + if (Platform.isIOS) { + await Future.delayed(const Duration(milliseconds: 250)); + } + String? result; + try { + result = await FilePicker.platform.getDirectoryPath(); + } catch (e) { + if (ctx.mounted) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar( + content: Text( + ctx.l10n.snackbarFolderPickerFailed(e.toString()), + ), + backgroundColor: Theme.of(ctx).colorScheme.error, + duration: const Duration(seconds: 4), + ), + ); + } + return; + } + if (result != null) { + if (Platform.isIOS) { + final validation = validateIosPath(result); + if (!validation.isValid) { + if (ctx.mounted) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar( + content: Text( + validation.errorReason ?? + context.l10n.setupIcloudNotSupported, + ), + backgroundColor: Theme.of(ctx).colorScheme.error, + duration: const Duration(seconds: 4), + ), + ); + } + return; + } + } + ref + .read(settingsProvider.notifier) + .setDownloadDirectory(result); + } + }, + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 16), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.tertiaryContainer.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.info_outline, size: 20, color: colorScheme.tertiary), + const SizedBox(width: 12), + Expanded( + child: Text( + context.l10n.setupIosEmptyFolderWarning, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onTertiaryContainer, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } + + void _showFormatEditor( + BuildContext context, + WidgetRef ref, + String current, { + void Function(String)? onSave, + String? title, + String? description, + }) { + final controller = TextEditingController(text: current); + final colorScheme = Theme.of(context).colorScheme; + + final basicTags = [ + '{artist}', '{title}', '{album}', '{track}', '{year}', '{date}', '{disc}', + ]; + final advancedTags = [ + '{track_raw}', '{track:02}', '{track:1}', + '{date:%Y}', '{date:%Y-%m-%d}', '{disc_raw}', '{disc:02}', + ]; + var showAdvancedTags = RegExp( + r'\{(?:track_raw|disc_raw|track:\d+|disc:\d+|date:[^}]+)\}', + caseSensitive: false, + ).hasMatch(current); + + void insertTag(String tag) { + final text = controller.text; + final selection = controller.selection; + final start = selection.start >= 0 ? selection.start : text.length; + final end = selection.end >= 0 ? selection.end : text.length; + String insertion = tag; + if (start > 0) { + final before = text.substring(0, start); + if (!before.trim().endsWith('-')) { + insertion = ' - $tag'; + } else if (before.trim().endsWith('-') && !before.endsWith(' ')) { + insertion = ' $tag'; + } + } + final newText = text.replaceRange(start, end, insertion); + controller.value = TextEditingValue( + text: newText, + selection: TextSelection.collapsed(offset: start + insertion.length), + ); + } + + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => StatefulBuilder( + builder: (context, setModalState) => Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + width: 32, + height: 4, + margin: const EdgeInsets.only(bottom: 24), + decoration: BoxDecoration( + color: colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Text( + title ?? context.l10n.filenameFormat, + style: Theme.of(context).textTheme.headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + description ?? context.l10n.downloadFilenameDescription, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + TextField( + controller: controller, + decoration: InputDecoration( + hintText: '{artist} - {title}', + filled: true, + fillColor: colorScheme.surfaceContainerHighest + .withValues(alpha: 0.3), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + ), + autofocus: true, + ), + const SizedBox(height: 24), + Text( + context.l10n.downloadFilenameInsertTag, + style: Theme.of(context).textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: basicTags.map((tag) { + return ActionChip( + label: Text(tag), + onPressed: () => insertTag(tag), + backgroundColor: colorScheme.surfaceContainerHighest + .withValues(alpha: 0.5), + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + labelStyle: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ); + }).toList(), + ), + const SizedBox(height: 12), + SwitchListTile( + value: showAdvancedTags, + onChanged: (value) => + setModalState(() => showAdvancedTags = value), + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.filenameShowAdvancedTags), + subtitle: Text( + context.l10n.filenameShowAdvancedTagsDescription, + ), + ), + if (showAdvancedTags) ...[ + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: advancedTags.map((tag) { + return ActionChip( + label: Text(tag), + onPressed: () => insertTag(tag), + backgroundColor: colorScheme.surfaceContainerHighest + .withValues(alpha: 0.5), + side: BorderSide.none, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + labelStyle: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ); + }).toList(), + ), + ], + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Text(context.l10n.dialogCancel), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: FilledButton( + onPressed: () { + final save = onSave ?? + ref + .read(settingsProvider.notifier) + .setFilenameFormat; + save(controller.text); + Navigator.pop(context); + }, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: Text(context.l10n.dialogSave), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + ), + ), + ), + ), + ), + ), + ); + } + + void _showAlbumFolderStructurePicker( + BuildContext context, + WidgetRef ref, + String current, + ) { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final option in [ + ('artist_album', context.l10n.albumFolderArtistAlbum, + context.l10n.albumFolderArtistAlbumSubtitle, + Icons.folder_outlined), + ('artist_year_album', context.l10n.albumFolderArtistYearAlbum, + context.l10n.albumFolderArtistYearAlbumSubtitle, + Icons.calendar_today_outlined), + ('album_only', context.l10n.albumFolderAlbumOnly, + context.l10n.albumFolderAlbumOnlySubtitle, Icons.album_outlined), + ('year_album', context.l10n.albumFolderYearAlbum, + context.l10n.albumFolderYearAlbumSubtitle, + Icons.event_outlined), + ('artist_album_singles', context.l10n.albumFolderArtistAlbumSingles, + context.l10n.albumFolderArtistAlbumSinglesSubtitle, + Icons.person_outlined), + ('artist_album_flat', context.l10n.albumFolderArtistAlbumFlat, + context.l10n.albumFolderArtistAlbumFlatSubtitle, + Icons.person_outline_outlined), + ]) + ListTile( + leading: Icon(option.$4), + title: Text(option.$2), + subtitle: Text(option.$3), + trailing: current == option.$1 + ? const Icon(Icons.check) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setAlbumFolderStructure(option.$1); + Navigator.pop(context); + }, + ), + ], + ), + ), + ); + } + + void _showFolderOrganizationPicker( + BuildContext context, + WidgetRef ref, + String current, + ) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + builder: (context) => SafeArea( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.downloadFolderOrganization, + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.folderOrganizationDescription, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + for (final option in [ + ('none', context.l10n.folderOrganizationNone, + context.l10n.folderOrganizationNoneSubtitle, + 'SpotiFLAC/Track.flac'), + ('playlist', context.l10n.folderOrganizationByPlaylist, + context.l10n.folderOrganizationByPlaylistSubtitle, + 'SpotiFLAC/Playlist Name/Track.flac'), + ('artist', context.l10n.folderOrganizationByArtist, + context.l10n.folderOrganizationByArtistSubtitle, + 'SpotiFLAC/Artist Name/Track.flac'), + ('album', context.l10n.folderOrganizationByAlbum, + context.l10n.folderOrganizationByAlbumSubtitle, + 'SpotiFLAC/Album Name/Track.flac'), + ('artist_album', context.l10n.folderOrganizationByArtistAlbum, + context.l10n.folderOrganizationByArtistAlbumSubtitle, + 'SpotiFLAC/Artist/Album/Track.flac'), + ]) + _FolderOption( + title: option.$2, + subtitle: option.$3, + example: option.$4, + isSelected: current == option.$1, + onTap: () { + ref + .read(settingsProvider.notifier) + .setFolderOrganization(option.$1); + Navigator.pop(context); + }, + ), + ], + ), + ), + ), + ); + } +} + +class _FolderOption extends StatelessWidget { + final String title; + final String subtitle; + final String example; + final bool isSelected; + final VoidCallback onTap; + const _FolderOption({ + required this.title, + required this.subtitle, + required this.example, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), + title: Text(title), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(subtitle), + const SizedBox(height: 4), + Text( + example, + style: TextStyle( + fontFamily: 'monospace', + fontSize: 11, + color: colorScheme.primary, + ), + ), + ], + ), + trailing: isSelected + ? Icon(Icons.check_circle, color: colorScheme.primary) + : Icon(Icons.circle_outlined, color: colorScheme.outline), + onTap: onTap, + ); + } +} diff --git a/lib/screens/settings/lyrics_settings_page.dart b/lib/screens/settings/lyrics_settings_page.dart new file mode 100644 index 00000000..862da242 --- /dev/null +++ b/lib/screens/settings/lyrics_settings_page.dart @@ -0,0 +1,373 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/app_bar_layout.dart'; +import 'package:spotiflac_android/screens/settings/lyrics_provider_priority_page.dart'; +import 'package:spotiflac_android/widgets/settings_group.dart'; + +class LyricsSettingsPage extends ConsumerWidget { + const LyricsSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final colorScheme = Theme.of(context).colorScheme; + final topPadding = normalizedHeaderTopPadding(context); + + return PopScope( + canPop: true, + child: Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 120 + topPadding, + collapsedHeight: kToolbarHeight, + floating: false, + pinned: true, + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final maxHeight = 120 + topPadding; + final minHeight = kToolbarHeight + topPadding; + final expandRatio = + ((constraints.maxHeight - minHeight) / + (maxHeight - minHeight)) + .clamp(0.0, 1.0); + final leftPadding = 56 - (32 * expandRatio); + return FlexibleSpaceBar( + expandedTitleScale: 1.0, + titlePadding: EdgeInsets.only( + left: leftPadding, + bottom: 16, + ), + title: Text( + context.l10n.settingsLyrics, + style: TextStyle( + fontSize: 20 + (8 * expandRatio), + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ); + }, + ), + ), + + // ── Lyrics Embedding ─────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionLyrics), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.subtitles_outlined, + title: context.l10n.optionsEmbedLyrics, + subtitle: settings.embedMetadata + ? context.l10n.optionsEmbedLyricsSubtitle + : context.l10n.downloadEmbedLyricsDisabled, + value: settings.embedLyrics, + enabled: settings.embedMetadata, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setEmbedLyrics(value), + showDivider: + settings.embedMetadata && settings.embedLyrics, + ), + if (settings.embedMetadata && settings.embedLyrics) ...[ + SettingsItem( + icon: Icons.lyrics_outlined, + title: context.l10n.lyricsMode, + subtitle: _getLyricsModeLabel( + context, + settings.lyricsMode, + ), + onTap: () => + _showLyricsModePicker(context, ref, settings.lyricsMode), + ), + SettingsItem( + icon: Icons.source_outlined, + title: context.l10n.lyricsProvidersTitle, + subtitle: _getLyricsProvidersSubtitle( + context, + settings.lyricsProviders, + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const LyricsProviderPriorityPage(), + ), + ), + showDivider: false, + ), + ], + ], + ), + ), + + // ── Provider Options ─────────────────────────────────────── + if (settings.embedMetadata && settings.embedLyrics) ...[ + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.sectionLyricsProviderOptions, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.translate_outlined, + title: context.l10n.downloadNeteaseIncludeTranslation, + subtitle: settings.lyricsIncludeTranslationNetease + ? context.l10n.downloadNeteaseIncludeTranslationEnabled + : context.l10n.downloadNeteaseIncludeTranslationDisabled, + value: settings.lyricsIncludeTranslationNetease, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setLyricsIncludeTranslationNetease(value), + ), + SettingsSwitchItem( + icon: Icons.text_fields_outlined, + title: context.l10n.downloadNeteaseIncludeRomanization, + subtitle: settings.lyricsIncludeRomanizationNetease + ? context + .l10n + .downloadNeteaseIncludeRomanizationEnabled + : context + .l10n + .downloadNeteaseIncludeRomanizationDisabled, + value: settings.lyricsIncludeRomanizationNetease, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setLyricsIncludeRomanizationNetease(value), + ), + SettingsSwitchItem( + icon: Icons.record_voice_over_outlined, + title: context.l10n.downloadAppleQqMultiPerson, + subtitle: settings.lyricsMultiPersonWordByWord + ? context.l10n.downloadAppleQqMultiPersonEnabled + : context.l10n.downloadAppleQqMultiPersonDisabled, + value: settings.lyricsMultiPersonWordByWord, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setLyricsMultiPersonWordByWord(value), + ), + SettingsItem( + icon: Icons.language_outlined, + title: context.l10n.downloadMusixmatchLanguage, + subtitle: settings.musixmatchLanguage.isEmpty + ? context.l10n.downloadMusixmatchLanguageAuto + : settings.musixmatchLanguage.toUpperCase(), + onTap: () => _showMusixmatchLanguagePicker( + context, + ref, + settings.musixmatchLanguage, + ), + showDivider: false, + ), + ], + ), + ), + ], + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + ], + ), + ), + ); + } + + String _getLyricsModeLabel(BuildContext context, String mode) { + switch (mode) { + case 'external': + return context.l10n.lyricsModeExternal; + case 'both': + return context.l10n.lyricsModeBoth; + default: + return context.l10n.lyricsModeEmbed; + } + } + + static const _providerDisplayNames = { + 'lrclib': 'LRCLIB', + 'netease': 'Netease', + 'musixmatch': 'Musixmatch', + 'apple_music': 'Apple Music', + 'qqmusic': 'QQ Music', + }; + + String _getLyricsProvidersSubtitle( + BuildContext context, + List providers, + ) { + if (providers.isEmpty) return context.l10n.downloadProvidersNoneEnabled; + return providers + .map((p) => _providerDisplayNames[p] ?? p) + .join(' > '); + } + + void _showLyricsModePicker( + BuildContext context, + WidgetRef ref, + String current, + ) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + 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.lyricsMode, + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.lyricsModeDescription, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + ListTile( + leading: const Icon(Icons.audiotrack), + title: Text(context.l10n.lyricsModeEmbed), + subtitle: Text(context.l10n.lyricsModeEmbedSubtitle), + trailing: current == 'embed' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setLyricsMode('embed'); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.insert_drive_file_outlined), + title: Text(context.l10n.lyricsModeExternal), + subtitle: Text(context.l10n.lyricsModeExternalSubtitle), + trailing: current == 'external' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setLyricsMode('external'); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.library_music_outlined), + title: Text(context.l10n.lyricsModeBoth), + subtitle: Text(context.l10n.lyricsModeBothSubtitle), + trailing: current == 'both' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setLyricsMode('both'); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + void _showMusixmatchLanguagePicker( + BuildContext context, + WidgetRef ref, + String currentLanguage, + ) { + final colorScheme = Theme.of(context).colorScheme; + final controller = TextEditingController(text: currentLanguage); + + showModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + isScrollControlled: true, + builder: (context) => Padding( + padding: EdgeInsets.only( + left: 24, + right: 24, + top: 24, + bottom: 24 + MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.downloadMusixmatchLanguage, + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + context.l10n.downloadMusixmatchLanguageDesc, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + const SizedBox(height: 16), + TextField( + controller: controller, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + labelText: context.l10n.downloadMusixmatchLanguageCode, + hintText: context.l10n.downloadMusixmatchLanguageHint, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.dialogCancel), + ), + const SizedBox(width: 8), + TextButton( + onPressed: () { + ref + .read(settingsProvider.notifier) + .setMusixmatchLanguage(''); + Navigator.pop(context); + }, + child: Text(context.l10n.downloadMusixmatchAuto), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: () { + final normalized = controller.text + .trim() + .toLowerCase() + .replaceAll(RegExp(r'[^a-z0-9\-_]'), ''); + ref + .read(settingsProvider.notifier) + .setMusixmatchLanguage(normalized); + Navigator.pop(context); + }, + child: Text(context.l10n.dialogSave), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/settings/metadata_settings_page.dart b/lib/screens/settings/metadata_settings_page.dart new file mode 100644 index 00000000..83f1afb1 --- /dev/null +++ b/lib/screens/settings/metadata_settings_page.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/providers/settings_provider.dart'; +import 'package:spotiflac_android/utils/app_bar_layout.dart'; +import 'package:spotiflac_android/utils/artist_utils.dart'; +import 'package:spotiflac_android/screens/settings/metadata_provider_priority_page.dart'; +import 'package:spotiflac_android/widgets/settings_group.dart'; + +class MetadataSettingsPage extends ConsumerWidget { + const MetadataSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final colorScheme = Theme.of(context).colorScheme; + final topPadding = normalizedHeaderTopPadding(context); + + return PopScope( + canPop: true, + child: Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 120 + topPadding, + collapsedHeight: kToolbarHeight, + floating: false, + pinned: true, + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + flexibleSpace: LayoutBuilder( + builder: (context, constraints) { + final maxHeight = 120 + topPadding; + final minHeight = kToolbarHeight + topPadding; + final expandRatio = + ((constraints.maxHeight - minHeight) / + (maxHeight - minHeight)) + .clamp(0.0, 1.0); + final leftPadding = 56 - (32 * expandRatio); + return FlexibleSpaceBar( + expandedTitleScale: 1.0, + titlePadding: EdgeInsets.only( + left: leftPadding, + bottom: 16, + ), + title: Text( + context.l10n.settingsMetadata, + style: TextStyle( + fontSize: 20 + (8 * expandRatio), + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ); + }, + ), + ), + + // ── Embedding ────────────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionDownload), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.sell_outlined, + title: 'Embed Metadata', + subtitle: settings.embedMetadata + ? 'Write metadata, cover art, and lyrics to files' + : 'Disabled (advanced): skip all metadata embedding', + value: settings.embedMetadata, + onChanged: (v) => + ref.read(settingsProvider.notifier).setEmbedMetadata(v), + showDivider: settings.embedMetadata, + ), + if (settings.embedMetadata) ...[ + SettingsItem( + icon: Icons.people_alt_outlined, + title: context.l10n.optionsArtistTagMode, + subtitle: _getArtistTagModeLabel( + context, + settings.artistTagMode, + ), + onTap: () => + _showArtistTagModePicker(context, ref, settings.artistTagMode), + ), + SettingsSwitchItem( + icon: Icons.image, + title: context.l10n.optionsMaxQualityCover, + subtitle: context.l10n.optionsMaxQualityCoverSubtitle, + value: settings.maxQualityCover, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setMaxQualityCover(v), + ), + SettingsSwitchItem( + icon: Icons.graphic_eq, + title: context.l10n.optionsReplayGain, + subtitle: settings.embedReplayGain + ? context.l10n.optionsReplayGainSubtitleOn + : context.l10n.optionsReplayGainSubtitleOff, + value: settings.embedReplayGain, + onChanged: (v) => ref + .read(settingsProvider.notifier) + .setEmbedReplayGain(v), + showDivider: false, + ), + ], + ], + ), + ), + + // ── Providers ───────────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.sectionMetadataProviders, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.source_outlined, + title: context.l10n.metadataProvidersTitle, + subtitle: context.l10n.metadataProvidersSubtitle, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const MetadataProviderPriorityPage(), + ), + ), + showDivider: false, + ), + ], + ), + ), + + // ── Deduplication ────────────────────────────────────────── + SliverToBoxAdapter( + child: SettingsSectionHeader( + title: context.l10n.sectionDuplicates, + ), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.filter_list_outlined, + title: context.l10n.downloadDeduplication, + subtitle: settings.deduplicateDownloads + ? context.l10n.downloadDeduplicationEnabled + : context.l10n.downloadDeduplicationDisabled, + value: settings.deduplicateDownloads, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setDeduplicateDownloads(value), + showDivider: false, + ), + ], + ), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + ], + ), + ), + ); + } + + String _getArtistTagModeLabel(BuildContext context, String mode) { + switch (mode) { + case artistTagModeSplitVorbis: + return context.l10n.optionsArtistTagModeSplitVorbis; + default: + return context.l10n.optionsArtistTagModeJoined; + } + } + + void _showArtistTagModePicker( + BuildContext context, + WidgetRef ref, + String currentMode, + ) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + 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.optionsArtistTagMode, + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.optionsArtistTagModeDescription, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), + ), + ), + ListTile( + leading: const Icon(Icons.segment_outlined), + title: Text(context.l10n.optionsArtistTagModeJoined), + subtitle: Text(context.l10n.optionsArtistTagModeJoinedSubtitle), + trailing: currentMode == artistTagModeJoined + ? const Icon(Icons.check) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setArtistTagMode(artistTagModeJoined); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.library_music_outlined), + title: Text(context.l10n.optionsArtistTagModeSplitVorbis), + subtitle: Text(context.l10n.optionsArtistTagModeSplitVorbisSubtitle), + trailing: currentMode == artistTagModeSplitVorbis + ? const Icon(Icons.check) + : null, + onTap: () { + ref + .read(settingsProvider.notifier) + .setArtistTagMode(artistTagModeSplitVorbis); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart deleted file mode 100644 index 147394aa..00000000 --- a/lib/screens/settings/options_settings_page.dart +++ /dev/null @@ -1,1012 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotiflac_android/l10n/l10n.dart'; -import 'package:spotiflac_android/providers/download_queue_provider.dart'; -import 'package:spotiflac_android/providers/extension_provider.dart'; -import 'package:spotiflac_android/providers/settings_provider.dart'; -import 'package:spotiflac_android/utils/app_bar_layout.dart'; -import 'package:spotiflac_android/utils/artist_utils.dart'; -import 'package:spotiflac_android/widgets/settings_group.dart'; - -class OptionsSettingsPage extends ConsumerWidget { - const OptionsSettingsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(settingsProvider); - final extensionState = ref.watch(extensionProvider); - final hasExtensions = extensionState.extensions.isNotEmpty; - final colorScheme = Theme.of(context).colorScheme; - final topPadding = normalizedHeaderTopPadding(context); - - return PopScope( - canPop: true, - child: Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - expandedHeight: 120 + topPadding, - collapsedHeight: kToolbarHeight, - floating: false, - pinned: true, - backgroundColor: colorScheme.surface, - surfaceTintColor: Colors.transparent, - leading: IconButton( - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), - ), - flexibleSpace: LayoutBuilder( - builder: (context, constraints) { - final maxHeight = 120 + topPadding; - final minHeight = kToolbarHeight + topPadding; - final expandRatio = - ((constraints.maxHeight - minHeight) / - (maxHeight - minHeight)) - .clamp(0.0, 1.0); - final leftPadding = 56 - (32 * expandRatio); - return FlexibleSpaceBar( - expandedTitleScale: 1.0, - titlePadding: EdgeInsets.only( - left: leftPadding, - bottom: 16, - ), - title: Text( - context.l10n.optionsTitle, - style: TextStyle( - fontSize: 20 + (8 * expandRatio), - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - ); - }, - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader( - title: context.l10n.sectionSearchSource, - ), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: const [ - _MetadataSourceSelector(), - _DefaultSearchTabSelector(), - ], - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader(title: context.l10n.sectionDownload), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.sync, - title: context.l10n.optionsAutoFallback, - subtitle: context.l10n.optionsAutoFallbackSubtitle, - value: settings.autoFallback, - 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), - ), - SettingsSwitchItem( - icon: Icons.sell_outlined, - title: 'Embed Metadata', - subtitle: settings.embedMetadata - ? 'Write metadata, cover art, and embedded lyrics to files' - : 'Disabled (advanced): skip all metadata embedding', - value: settings.embedMetadata, - onChanged: (v) => - ref.read(settingsProvider.notifier).setEmbedMetadata(v), - showDivider: settings.embedMetadata, - ), - if (settings.embedMetadata) - SettingsItem( - icon: Icons.people_alt_outlined, - title: context.l10n.optionsArtistTagMode, - subtitle: _getArtistTagModeLabel( - context, - settings.artistTagMode, - ), - onTap: () => _showArtistTagModePicker( - context, - ref, - settings.artistTagMode, - ), - ), - SettingsSwitchItem( - icon: Icons.image, - title: context.l10n.optionsMaxQualityCover, - subtitle: settings.embedMetadata - ? context.l10n.optionsMaxQualityCoverSubtitle - : 'Disabled when metadata embedding is off', - value: settings.maxQualityCover, - enabled: settings.embedMetadata, - onChanged: (v) => ref - .read(settingsProvider.notifier) - .setMaxQualityCover(v), - ), - SettingsSwitchItem( - icon: Icons.graphic_eq, - title: context.l10n.optionsReplayGain, - subtitle: settings.embedReplayGain - ? context.l10n.optionsReplayGainSubtitleOn - : context.l10n.optionsReplayGainSubtitleOff, - value: settings.embedReplayGain, - enabled: settings.embedMetadata, - onChanged: (v) => ref - .read(settingsProvider.notifier) - .setEmbedReplayGain(v), - showDivider: false, - ), - ], - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader( - title: context.l10n.sectionPerformance, - ), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - _ConcurrentDownloadsItem( - currentValue: settings.concurrentDownloads, - onChanged: (v) => ref - .read(settingsProvider.notifier) - .setConcurrentDownloads(v), - ), - ], - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader(title: context.l10n.sectionApp), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.extension, - title: context.l10n.optionsExtensionStore, - subtitle: context.l10n.optionsExtensionStoreSubtitle, - value: settings.showExtensionStore, - onChanged: (v) => ref - .read(settingsProvider.notifier) - .setShowExtensionStore(v), - ), - SettingsSwitchItem( - icon: Icons.system_update, - title: context.l10n.optionsCheckUpdates, - subtitle: context.l10n.optionsCheckUpdatesSubtitle, - value: settings.checkForUpdates, - onChanged: (v) => ref - .read(settingsProvider.notifier) - .setCheckForUpdates(v), - ), - _UpdateChannelSelector( - currentChannel: settings.updateChannel, - onChanged: (v) => - ref.read(settingsProvider.notifier).setUpdateChannel(v), - ), - ], - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader(title: context.l10n.sectionData), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsItem( - icon: Icons.cleaning_services_outlined, - title: context.l10n.cleanupOrphanedDownloads, - subtitle: context.l10n.cleanupOrphanedDownloadsSubtitle, - onTap: () => _cleanupOrphanedDownloads(context, ref), - ), - SettingsItem( - icon: Icons.delete_forever, - title: context.l10n.optionsClearHistory, - subtitle: context.l10n.optionsClearHistorySubtitle, - onTap: () => - _showClearHistoryDialog(context, ref, colorScheme), - showDivider: false, - ), - ], - ), - ), - - SliverToBoxAdapter( - child: SettingsSectionHeader(title: context.l10n.sectionDebug), - ), - SliverToBoxAdapter( - child: SettingsGroup( - children: [ - SettingsSwitchItem( - icon: Icons.bug_report, - title: context.l10n.optionsDetailedLogging, - subtitle: settings.enableLogging - ? context.l10n.optionsDetailedLoggingOn - : context.l10n.optionsDetailedLoggingOff, - value: settings.enableLogging, - onChanged: (v) => - ref.read(settingsProvider.notifier).setEnableLogging(v), - showDivider: false, - ), - ], - ), - ), - - const SliverToBoxAdapter(child: SizedBox(height: 32)), - ], - ), - ), - ); - } - - String _getArtistTagModeLabel(BuildContext context, String mode) { - switch (mode) { - case artistTagModeSplitVorbis: - return context.l10n.optionsArtistTagModeSplitVorbis; - default: - return context.l10n.optionsArtistTagModeJoined; - } - } - - void _showArtistTagModePicker( - BuildContext context, - WidgetRef ref, - String currentMode, - ) { - final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( - 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.optionsArtistTagMode, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), - child: Text( - context.l10n.optionsArtistTagModeDescription, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ListTile( - leading: const Icon(Icons.segment_outlined), - title: Text(context.l10n.optionsArtistTagModeJoined), - subtitle: Text(context.l10n.optionsArtistTagModeJoinedSubtitle), - trailing: currentMode == artistTagModeJoined - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setArtistTagMode(artistTagModeJoined); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.library_music_outlined), - title: Text(context.l10n.optionsArtistTagModeSplitVorbis), - subtitle: Text( - context.l10n.optionsArtistTagModeSplitVorbisSubtitle, - ), - trailing: currentMode == artistTagModeSplitVorbis - ? const Icon(Icons.check) - : null, - onTap: () { - ref - .read(settingsProvider.notifier) - .setArtistTagMode(artistTagModeSplitVorbis); - Navigator.pop(context); - }, - ), - const SizedBox(height: 16), - ], - ), - ), - ); - } - - void _showClearHistoryDialog( - BuildContext context, - WidgetRef ref, - ColorScheme colorScheme, - ) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.l10n.dialogClearHistoryTitle), - content: Text(context.l10n.dialogClearHistoryMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.dialogCancel), - ), - TextButton( - onPressed: () { - ref.read(downloadHistoryProvider.notifier).clearHistory(); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.snackbarHistoryCleared)), - ); - }, - child: Text( - context.l10n.dialogClear, - style: TextStyle(color: colorScheme.error), - ), - ), - ], - ), - ); - } - - Future _cleanupOrphanedDownloads( - BuildContext context, - WidgetRef ref, - ) async { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - content: Row( - children: [ - const CircularProgressIndicator(), - const SizedBox(width: 16), - Text(context.l10n.cleanupOrphanedDownloads), - ], - ), - ), - ); - - try { - final removed = await ref - .read(downloadHistoryProvider.notifier) - .cleanupOrphanedDownloads(); - - if (context.mounted) { - Navigator.pop(context); // Close loading dialog - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - removed > 0 - ? context.l10n.cleanupOrphanedDownloadsResult(removed) - : context.l10n.cleanupOrphanedDownloadsNone, - ), - ), - ); - } - } catch (e) { - if (context.mounted) { - Navigator.pop(context); // Close loading dialog - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), - ); - } - } - } -} - -class _ConcurrentDownloadsItem extends StatelessWidget { - final int currentValue; - final ValueChanged onChanged; - const _ConcurrentDownloadsItem({ - required this.currentValue, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.download_for_offline, - color: colorScheme.onSurfaceVariant, - size: 24, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.optionsConcurrentDownloads, - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(height: 2), - Text( - currentValue == 1 - ? context.l10n.optionsConcurrentSequential - : context.l10n.optionsConcurrentParallel( - currentValue, - ), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - _ConcurrentChip( - label: '1', - isSelected: currentValue == 1, - onTap: () => onChanged(1), - ), - const SizedBox(width: 8), - _ConcurrentChip( - label: '2', - isSelected: currentValue == 2, - onTap: () => onChanged(2), - ), - const SizedBox(width: 8), - _ConcurrentChip( - label: '3', - isSelected: currentValue == 3, - onTap: () => onChanged(3), - ), - const SizedBox(width: 8), - _ConcurrentChip( - label: '4', - isSelected: currentValue == 4, - onTap: () => onChanged(4), - ), - const SizedBox(width: 8), - _ConcurrentChip( - label: '5', - isSelected: currentValue == 5, - onTap: () => onChanged(5), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Icon( - Icons.warning_amber_rounded, - size: 16, - color: colorScheme.error, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - context.l10n.optionsConcurrentWarning, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: colorScheme.error), - ), - ), - ], - ), - ], - ), - ); - } -} - -class _ConcurrentChip extends StatelessWidget { - final String label; - final bool isSelected; - final VoidCallback onTap; - const _ConcurrentChip({ - required this.label, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - final unselectedColor = isDark - ? Color.alphaBlend( - Colors.white.withValues(alpha: 0.05), - colorScheme.surface, - ) - : colorScheme.surfaceContainerHigh; - - return Expanded( - child: 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), - child: Center( - child: Text( - label, - style: TextStyle( - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ), - ), - ); - } -} - -class _UpdateChannelSelector extends StatelessWidget { - final String currentChannel; - final ValueChanged onChanged; - const _UpdateChannelSelector({ - required this.currentChannel, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.new_releases, - color: colorScheme.onSurfaceVariant, - size: 24, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.optionsUpdateChannel, - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(height: 2), - Text( - currentChannel == 'preview' - ? context.l10n.optionsUpdateChannelPreview - : context.l10n.optionsUpdateChannelStable, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - _ChannelChip( - label: context.l10n.channelStable, - isSelected: currentChannel == 'stable', - onTap: () => onChanged('stable'), - ), - const SizedBox(width: 8), - _ChannelChip( - label: context.l10n.channelPreview, - isSelected: currentChannel == 'preview', - onTap: () => onChanged('preview'), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Icon( - Icons.info_outline, - size: 16, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - context.l10n.optionsUpdateChannelWarning, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ], - ), - ); - } -} - -class _ChannelChip extends StatelessWidget { - final String label; - final bool isSelected; - final VoidCallback onTap; - const _ChannelChip({ - required this.label, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - final unselectedColor = isDark - ? Color.alphaBlend( - Colors.white.withValues(alpha: 0.05), - colorScheme.surface, - ) - : colorScheme.surfaceContainerHigh; - - return Expanded( - child: 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), - child: Center( - child: Text( - label, - style: TextStyle( - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ), - ), - ); - } -} - -class _MetadataSourceSelector extends ConsumerWidget { - const _MetadataSourceSelector(); - - static const _builtInProviders = {'tidal': 'Tidal', 'qobuz': 'Qobuz'}; - - Extension? _defaultSearchExtension(List extensions) { - return extensions - .where( - (ext) => - ext.enabled && - ext.hasCustomSearch && - ext.searchBehavior?.primary == true, - ) - .firstOrNull ?? - extensions - .where((ext) => ext.enabled && ext.hasCustomSearch) - .firstOrNull; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final colorScheme = Theme.of(context).colorScheme; - final settings = ref.watch(settingsProvider); - 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'; - final defaultProviderLabel = - '${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)'; - final searchProvider = - isValidBuiltIn || - extState.extensions.any( - (e) => - e.enabled && e.hasCustomSearch && e.id == rawSearchProvider, - ) - ? rawSearchProvider - : ''; - final isBuiltIn = _builtInProviders.containsKey(searchProvider); - - Extension? activeExtension; - if (searchProvider.isNotEmpty && !isBuiltIn) { - activeExtension = extState.extensions - .where((e) => e.id == searchProvider && e.enabled) - .firstOrNull; - } - final hasNonDefaultProvider = isBuiltIn || activeExtension != null; - - String subtitle; - if (isBuiltIn) { - subtitle = 'Using ${_builtInProviders[searchProvider]}'; - } else if (activeExtension != null) { - subtitle = context.l10n.optionsUsingExtension( - activeExtension.displayName, - ); - } else { - subtitle = context.l10n.optionsPrimaryProviderSubtitle; - } - - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.optionsPrimaryProvider, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ), - const SizedBox(height: 4), - Text( - subtitle, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: hasNonDefaultProvider - ? colorScheme.primary - : colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _SourceChip( - icon: Icons.graphic_eq, - label: defaultProviderLabel, - isSelected: searchProvider.isEmpty, - onTap: () { - if (hasNonDefaultProvider) { - ref - .read(settingsProvider.notifier) - .setSearchProvider(null); - } - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _SourceChip( - icon: Icons.waves, - label: 'Tidal', - isSelected: searchProvider == 'tidal', - onTap: () { - ref - .read(settingsProvider.notifier) - .setSearchProvider('tidal'); - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _SourceChip( - icon: Icons.album, - label: 'Qobuz', - isSelected: searchProvider == 'qobuz', - onTap: () { - ref - .read(settingsProvider.notifier) - .setSearchProvider('qobuz'); - }, - ), - ), - ], - ), - if (activeExtension != null) ...[ - const SizedBox(height: 12), - Row( - children: [ - Icon( - Icons.info_outline, - size: 16, - color: colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Tap $defaultProviderLabel to switch back from extension', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ], - ], - ), - ); - } -} - -class _DefaultSearchTabSelector extends ConsumerWidget { - const _DefaultSearchTabSelector(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final colorScheme = Theme.of(context).colorScheme; - final selectedTab = ref.watch( - settingsProvider.select((s) => s.defaultSearchTab), - ); - - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - 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: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _SourceChip( - icon: Icons.dashboard_outlined, - label: context.l10n.historyFilterAll, - isSelected: selectedTab == 'all', - onTap: () => ref - .read(settingsProvider.notifier) - .setDefaultSearchTab('all'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: _SourceChip( - icon: Icons.music_note, - label: context.l10n.searchSongs, - isSelected: selectedTab == 'track', - onTap: () => ref - .read(settingsProvider.notifier) - .setDefaultSearchTab('track'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: _SourceChip( - icon: Icons.person, - label: context.l10n.searchArtists, - isSelected: selectedTab == 'artist', - onTap: () => ref - .read(settingsProvider.notifier) - .setDefaultSearchTab('artist'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: _SourceChip( - icon: Icons.album, - label: context.l10n.searchAlbums, - isSelected: selectedTab == 'album', - onTap: () => ref - .read(settingsProvider.notifier) - .setDefaultSearchTab('album'), - ), - ), - ], - ), - ], - ), - ); - } -} - -class _SourceChip extends StatelessWidget { - final IconData icon; - final String label; - final bool isSelected; - final VoidCallback? onTap; - - const _SourceChip({ - required this.icon, - required this.label, - required this.isSelected, - this.onTap, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - 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: 14, horizontal: 18), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 28, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 6), - Text( - label, - style: TextStyle( - fontSize: 12, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index f08d967a..97b51288 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -4,9 +4,12 @@ import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/screens/settings/appearance_settings_page.dart'; import 'package:spotiflac_android/screens/settings/download_settings_page.dart'; +import 'package:spotiflac_android/screens/settings/files_settings_page.dart'; +import 'package:spotiflac_android/screens/settings/lyrics_settings_page.dart'; +import 'package:spotiflac_android/screens/settings/metadata_settings_page.dart'; import 'package:spotiflac_android/screens/settings/extensions_page.dart'; import 'package:spotiflac_android/screens/settings/library_settings_page.dart'; -import 'package:spotiflac_android/screens/settings/options_settings_page.dart'; +import 'package:spotiflac_android/screens/settings/app_settings_page.dart'; import 'package:spotiflac_android/screens/settings/about_page.dart'; import 'package:spotiflac_android/screens/settings/cache_management_page.dart'; import 'package:spotiflac_android/screens/settings/donate_page.dart'; @@ -48,7 +51,7 @@ class SettingsTab extends ConsumerWidget { title: Text( context.l10n.settingsTitle, style: TextStyle( - fontSize: 20 + (14 * expandRatio), // 20 -> 34 + fontSize: 20 + (14 * expandRatio), fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), @@ -58,6 +61,7 @@ class SettingsTab extends ConsumerWidget { ), ), + // ── Group 1: Appearance & Content ────────────────────────────── SliverToBoxAdapter( child: Builder( builder: (context) { @@ -72,6 +76,34 @@ class SettingsTab extends ConsumerWidget { onTap: () => _navigateTo(context, const AppearanceSettingsPage()), ), + SettingsItem( + icon: Icons.library_music_outlined, + title: l10n.settingsLocalLibrary, + subtitle: l10n.settingsLocalLibrarySubtitle, + onTap: () => + _navigateTo(context, const LibrarySettingsPage()), + ), + SettingsItem( + icon: Icons.extension_outlined, + title: l10n.settingsExtensions, + subtitle: l10n.settingsExtensionsSubtitle, + onTap: () => _navigateTo(context, const ExtensionsPage()), + showDivider: false, + ), + ], + ); + }, + ), + ), + + // ── Group 2: Download ────────────────────────────────────────── + SliverToBoxAdapter( + child: Builder( + builder: (context) { + final l10n = context.l10n; + return SettingsGroup( + margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), + children: [ SettingsItem( icon: Icons.download_outlined, title: l10n.settingsDownload, @@ -80,12 +112,41 @@ class SettingsTab extends ConsumerWidget { _navigateTo(context, const DownloadSettingsPage()), ), SettingsItem( - icon: Icons.library_music_outlined, - title: l10n.settingsLocalLibrary, - subtitle: l10n.settingsLocalLibrarySubtitle, + icon: Icons.folder_outlined, + title: l10n.settingsFiles, + subtitle: l10n.settingsFilesSubtitle, onTap: () => - _navigateTo(context, const LibrarySettingsPage()), + _navigateTo(context, const FilesSettingsPage()), ), + SettingsItem( + icon: Icons.sell_outlined, + title: l10n.settingsMetadata, + subtitle: l10n.settingsMetadataSubtitle, + onTap: () => + _navigateTo(context, const MetadataSettingsPage()), + ), + SettingsItem( + icon: Icons.lyrics_outlined, + title: l10n.settingsLyrics, + subtitle: l10n.settingsLyricsSubtitle, + onTap: () => + _navigateTo(context, const LyricsSettingsPage()), + showDivider: false, + ), + ], + ); + }, + ), + ), + + // ── Group 3: App ─────────────────────────────────────────────── + SliverToBoxAdapter( + child: Builder( + builder: (context) { + final l10n = context.l10n; + return SettingsGroup( + margin: const EdgeInsets.fromLTRB(16, 4, 16, 4), + children: [ SettingsItem( icon: Icons.storage_outlined, title: l10n.settingsCache, @@ -95,41 +156,22 @@ class SettingsTab extends ConsumerWidget { ), SettingsItem( icon: Icons.tune_outlined, - title: l10n.settingsOptions, - subtitle: l10n.settingsOptionsSubtitle, + title: l10n.settingsApp, + subtitle: l10n.settingsAppSubtitle, onTap: () => - _navigateTo(context, const OptionsSettingsPage()), + _navigateTo(context, const AppSettingsPage()), ), SettingsItem( - icon: Icons.extension_outlined, - title: l10n.settingsExtensions, - subtitle: l10n.settingsExtensionsSubtitle, - onTap: () => _navigateTo(context, const ExtensionsPage()), + icon: Icons.article_outlined, + title: l10n.logTitle, + subtitle: l10n.settingsLogsSubtitle, + onTap: () => _navigateTo(context, const LogScreen()), ), SettingsItem( icon: Icons.favorite_outline, title: l10n.settingsDonate, subtitle: l10n.settingsDonateSubtitle, onTap: () => _navigateTo(context, const DonatePage()), - showDivider: false, - ), - ], - ); - }, - ), - ), - - SliverToBoxAdapter( - child: Builder( - builder: (context) { - final l10n = context.l10n; - return SettingsGroup( - children: [ - SettingsItem( - icon: Icons.article_outlined, - title: l10n.logTitle, - subtitle: l10n.settingsLogsSubtitle, - onTap: () => _navigateTo(context, const LogScreen()), ), SettingsItem( icon: Icons.info_outline, diff --git a/site/icon.png b/site/icon.png index 565fab55..49bbdb40 100644 Binary files a/site/icon.png and b/site/icon.png differ