mirror of
https://github.com/spotiflacapp/SpotiFLAC-Mobile.git
synced 2026-05-31 19:05:05 +07:00
feat: add multilanguage support (i18n) for English and Indonesian
- Add flutter_localizations and intl dependencies - Create l10n.yaml configuration and ARB files (app_en.arb, app_id.arb) - Add L10n extension for easy context.l10n access - Localize all active screens: - setup_screen, track_metadata_screen, log_screen - download_settings_page, options_settings_page, appearance_settings_page - extensions_page, extension_detail_page, extension_details_screen - about_page, provider_priority_page, metadata_provider_priority_page - home_tab, queue_tab, store_tab, main_shell - album_screen, artist_screen, playlist_screen - downloaded_album_screen, queue_screen - Localize widgets: update_dialog, download_service_picker - Technical terms (FLAC, API, Spotify, Tidal, Qobuz, etc.) are NOT translated - ~900+ localized strings in English, ~660+ in Indonesian
This commit is contained in:
parent
7c6705c75c
commit
f26af38c1e
34 changed files with 9758 additions and 601 deletions
6
l10n.yaml
Normal file
6
l10n.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
arb-dir: lib/l10n/arb
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: AppLocalizations
|
||||
output-dir: lib/l10n
|
||||
nullable-getter: false
|
||||
10
lib/app.dart
10
lib/app.dart
|
|
@ -1,10 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:spotiflac_android/screens/main_shell.dart';
|
||||
import 'package:spotiflac_android/screens/setup_screen.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
|
||||
final _routerProvider = Provider<GoRouter>((ref) {
|
||||
// Only watch isFirstLaunch to prevent router rebuild on other settings changes
|
||||
|
|
@ -43,6 +45,14 @@ class SpotiFLACApp extends ConsumerWidget {
|
|||
themeAnimationDuration: const Duration(milliseconds: 300),
|
||||
themeAnimationCurve: Curves.easeInOut,
|
||||
routerConfig: router,
|
||||
// Localization
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
3578
lib/l10n/app_localizations.dart
Normal file
3578
lib/l10n/app_localizations.dart
Normal file
File diff suppressed because it is too large
Load diff
1961
lib/l10n/app_localizations_en.dart
Normal file
1961
lib/l10n/app_localizations_en.dart
Normal file
File diff suppressed because it is too large
Load diff
1974
lib/l10n/app_localizations_id.dart
Normal file
1974
lib/l10n/app_localizations_id.dart
Normal file
File diff suppressed because it is too large
Load diff
910
lib/l10n/arb/app_en.arb
Normal file
910
lib/l10n/arb/app_en.arb
Normal file
|
|
@ -0,0 +1,910 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"@@last_modified": "2026-01-16",
|
||||
|
||||
"appName": "SpotiFLAC",
|
||||
"appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
|
||||
"navHome": "Home",
|
||||
"navHistory": "History",
|
||||
"navSettings": "Settings",
|
||||
"navStore": "Store",
|
||||
|
||||
"homeTitle": "Home",
|
||||
"homeSearchHint": "Paste Spotify URL or search...",
|
||||
"homeSearchHintExtension": "Search with {extensionName}...",
|
||||
"@homeSearchHintExtension": {
|
||||
"placeholders": {
|
||||
"extensionName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"homeSubtitle": "Paste a Spotify link or search by name",
|
||||
"homeSupports": "Supports: Track, Album, Playlist, Artist URLs",
|
||||
"homeRecent": "Recent",
|
||||
|
||||
"historyTitle": "History",
|
||||
"historyDownloading": "Downloading ({count})",
|
||||
"@historyDownloading": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"historyDownloaded": "Downloaded",
|
||||
"historyFilterAll": "All",
|
||||
"historyFilterAlbums": "Albums",
|
||||
"historyFilterSingles": "Singles",
|
||||
"historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}",
|
||||
"@historyTracksCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}",
|
||||
"@historyAlbumsCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"historyNoDownloads": "No download history",
|
||||
"historyNoDownloadsSubtitle": "Downloaded tracks will appear here",
|
||||
"historyNoAlbums": "No album downloads",
|
||||
"historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here",
|
||||
"historyNoSingles": "No single downloads",
|
||||
"historyNoSinglesSubtitle": "Single track downloads will appear here",
|
||||
|
||||
"settingsTitle": "Settings",
|
||||
"settingsDownload": "Download",
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsOptions": "Options",
|
||||
"settingsExtensions": "Extensions",
|
||||
"settingsAbout": "About",
|
||||
|
||||
"downloadTitle": "Download",
|
||||
"downloadLocation": "Download Location",
|
||||
"downloadLocationSubtitle": "Choose where to save files",
|
||||
"downloadLocationDefault": "Default location",
|
||||
"downloadDefaultService": "Default Service",
|
||||
"downloadDefaultServiceSubtitle": "Service used for downloads",
|
||||
"downloadDefaultQuality": "Default Quality",
|
||||
"downloadAskQuality": "Ask Quality Before Download",
|
||||
"downloadAskQualitySubtitle": "Show quality picker for each download",
|
||||
"downloadFilenameFormat": "Filename Format",
|
||||
"downloadFolderOrganization": "Folder Organization",
|
||||
"downloadSeparateSingles": "Separate Singles",
|
||||
"downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder",
|
||||
|
||||
"qualityBest": "Best Available",
|
||||
"qualityFlac": "FLAC",
|
||||
"quality320": "320 kbps",
|
||||
"quality128": "128 kbps",
|
||||
|
||||
"appearanceTitle": "Appearance",
|
||||
"appearanceTheme": "Theme",
|
||||
"appearanceThemeSystem": "System",
|
||||
"appearanceThemeLight": "Light",
|
||||
"appearanceThemeDark": "Dark",
|
||||
"appearanceDynamicColor": "Dynamic Color",
|
||||
"appearanceDynamicColorSubtitle": "Use colors from your wallpaper",
|
||||
"appearanceAccentColor": "Accent Color",
|
||||
"appearanceHistoryView": "History View",
|
||||
"appearanceHistoryViewList": "List",
|
||||
"appearanceHistoryViewGrid": "Grid",
|
||||
|
||||
"optionsTitle": "Options",
|
||||
"optionsSearchSource": "Search Source",
|
||||
"optionsPrimaryProvider": "Primary Provider",
|
||||
"optionsPrimaryProviderSubtitle": "Service used when searching by track name.",
|
||||
"optionsUsingExtension": "Using extension: {extensionName}",
|
||||
"@optionsUsingExtension": {
|
||||
"placeholders": {
|
||||
"extensionName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension",
|
||||
"optionsAutoFallback": "Auto Fallback",
|
||||
"optionsAutoFallbackSubtitle": "Try other services if download fails",
|
||||
"optionsUseExtensionProviders": "Use Extension Providers",
|
||||
"optionsUseExtensionProvidersOn": "Extensions will be tried first",
|
||||
"optionsUseExtensionProvidersOff": "Using built-in providers only",
|
||||
"optionsEmbedLyrics": "Embed Lyrics",
|
||||
"optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files",
|
||||
"optionsMaxQualityCover": "Max Quality Cover",
|
||||
"optionsMaxQualityCoverSubtitle": "Download highest resolution cover art",
|
||||
"optionsConcurrentDownloads": "Concurrent Downloads",
|
||||
"optionsConcurrentSequential": "Sequential (1 at a time)",
|
||||
"optionsConcurrentParallel": "{count} parallel downloads",
|
||||
"@optionsConcurrentParallel": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"optionsConcurrentWarning": "Parallel downloads may trigger rate limiting",
|
||||
"optionsExtensionStore": "Extension Store",
|
||||
"optionsExtensionStoreSubtitle": "Show Store tab in navigation",
|
||||
"optionsCheckUpdates": "Check for Updates",
|
||||
"optionsCheckUpdatesSubtitle": "Notify when new version is available",
|
||||
"optionsUpdateChannel": "Update Channel",
|
||||
"optionsUpdateChannelStable": "Stable releases only",
|
||||
"optionsUpdateChannelPreview": "Get preview releases",
|
||||
"optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features",
|
||||
"optionsClearHistory": "Clear Download History",
|
||||
"optionsClearHistorySubtitle": "Remove all downloaded tracks from history",
|
||||
"optionsDetailedLogging": "Detailed Logging",
|
||||
"optionsDetailedLoggingOn": "Detailed logs are being recorded",
|
||||
"optionsDetailedLoggingOff": "Enable for bug reports",
|
||||
"optionsSpotifyCredentials": "Spotify Credentials",
|
||||
"optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...",
|
||||
"@optionsSpotifyCredentialsConfigured": {
|
||||
"placeholders": {
|
||||
"clientId": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"optionsSpotifyCredentialsRequired": "Required - tap to configure",
|
||||
"optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com",
|
||||
|
||||
"extensionsTitle": "Extensions",
|
||||
"extensionsInstalled": "Installed Extensions",
|
||||
"extensionsNone": "No extensions installed",
|
||||
"extensionsNoneSubtitle": "Install extensions from the Store tab",
|
||||
"extensionsEnabled": "Enabled",
|
||||
"extensionsDisabled": "Disabled",
|
||||
"extensionsVersion": "Version {version}",
|
||||
"@extensionsVersion": {
|
||||
"placeholders": {
|
||||
"version": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"extensionsAuthor": "by {author}",
|
||||
"@extensionsAuthor": {
|
||||
"placeholders": {
|
||||
"author": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"extensionsUninstall": "Uninstall",
|
||||
"extensionsSetAsSearch": "Set as Search Provider",
|
||||
|
||||
"storeTitle": "Extension Store",
|
||||
"storeSearch": "Search extensions...",
|
||||
"storeInstall": "Install",
|
||||
"storeInstalled": "Installed",
|
||||
"storeUpdate": "Update",
|
||||
|
||||
"aboutTitle": "About",
|
||||
"aboutContributors": "Contributors",
|
||||
"aboutMobileDeveloper": "Mobile version developer",
|
||||
"aboutOriginalCreator": "Creator of the original SpotiFLAC",
|
||||
"aboutLogoArtist": "The talented artist who created our beautiful app logo!",
|
||||
"aboutSpecialThanks": "Special Thanks",
|
||||
"aboutLinks": "Links",
|
||||
"aboutMobileSource": "Mobile source code",
|
||||
"aboutPCSource": "PC source code",
|
||||
"aboutReportIssue": "Report an issue",
|
||||
"aboutReportIssueSubtitle": "Report any problems you encounter",
|
||||
"aboutFeatureRequest": "Feature request",
|
||||
"aboutFeatureRequestSubtitle": "Suggest new features for the app",
|
||||
"aboutSupport": "Support",
|
||||
"aboutBuyMeCoffee": "Buy me a coffee",
|
||||
"aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi",
|
||||
"aboutApp": "App",
|
||||
"aboutVersion": "Version",
|
||||
|
||||
"albumTitle": "Album",
|
||||
"albumTracks": "{count, plural, =1{1 track} other{{count} tracks}}",
|
||||
"@albumTracks": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"albumDownloadAll": "Download All",
|
||||
"albumDownloadRemaining": "Download Remaining",
|
||||
|
||||
"playlistTitle": "Playlist",
|
||||
"artistTitle": "Artist",
|
||||
"artistAlbums": "Albums",
|
||||
"artistSingles": "Singles & EPs",
|
||||
|
||||
"trackMetadataTitle": "Track Info",
|
||||
"trackMetadataArtist": "Artist",
|
||||
"trackMetadataAlbum": "Album",
|
||||
"trackMetadataDuration": "Duration",
|
||||
"trackMetadataQuality": "Quality",
|
||||
"trackMetadataPath": "File Path",
|
||||
"trackMetadataDownloadedAt": "Downloaded",
|
||||
"trackMetadataService": "Service",
|
||||
"trackMetadataPlay": "Play",
|
||||
"trackMetadataShare": "Share",
|
||||
"trackMetadataDelete": "Delete",
|
||||
"trackMetadataRedownload": "Re-download",
|
||||
"trackMetadataOpenFolder": "Open Folder",
|
||||
|
||||
"setupTitle": "Welcome to SpotiFLAC",
|
||||
"setupSubtitle": "Let's get you started",
|
||||
"setupStoragePermission": "Storage Permission",
|
||||
"setupStoragePermissionSubtitle": "Required to save downloaded files",
|
||||
"setupStoragePermissionGranted": "Permission granted",
|
||||
"setupStoragePermissionDenied": "Permission denied",
|
||||
"setupGrantPermission": "Grant Permission",
|
||||
"setupDownloadLocation": "Download Location",
|
||||
"setupChooseFolder": "Choose Folder",
|
||||
"setupContinue": "Continue",
|
||||
"setupSkip": "Skip for now",
|
||||
|
||||
"dialogCancel": "Cancel",
|
||||
"dialogOk": "OK",
|
||||
"dialogSave": "Save",
|
||||
"dialogDelete": "Delete",
|
||||
"dialogRetry": "Retry",
|
||||
"dialogClose": "Close",
|
||||
"dialogYes": "Yes",
|
||||
"dialogNo": "No",
|
||||
"dialogClear": "Clear",
|
||||
"dialogConfirm": "Confirm",
|
||||
"dialogDone": "Done",
|
||||
|
||||
"dialogClearHistoryTitle": "Clear History",
|
||||
"dialogClearHistoryMessage": "Are you sure you want to clear all download history? This cannot be undone.",
|
||||
"dialogDeleteSelectedTitle": "Delete Selected",
|
||||
"dialogDeleteSelectedMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from history?\n\nThis will also delete the files from storage.",
|
||||
"@dialogDeleteSelectedMessage": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"dialogImportPlaylistTitle": "Import Playlist",
|
||||
"dialogImportPlaylistMessage": "Found {count} tracks in CSV. Add them to download queue?",
|
||||
"@dialogImportPlaylistMessage": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
|
||||
"snackbarAddedToQueue": "Added \"{trackName}\" to queue",
|
||||
"@snackbarAddedToQueue": {
|
||||
"placeholders": {
|
||||
"trackName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"snackbarAddedTracksToQueue": "Added {count} tracks to queue",
|
||||
"@snackbarAddedTracksToQueue": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"snackbarAlreadyDownloaded": "\"{trackName}\" already downloaded",
|
||||
"@snackbarAlreadyDownloaded": {
|
||||
"placeholders": {
|
||||
"trackName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"snackbarHistoryCleared": "History cleared",
|
||||
"snackbarCredentialsSaved": "Credentials saved",
|
||||
"snackbarCredentialsCleared": "Credentials cleared",
|
||||
"snackbarDeletedTracks": "Deleted {count} {count, plural, =1{track} other{tracks}}",
|
||||
"@snackbarDeletedTracks": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"snackbarCannotOpenFile": "Cannot open file: {error}",
|
||||
"@snackbarCannotOpenFile": {
|
||||
"placeholders": {
|
||||
"error": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"snackbarFillAllFields": "Please fill all fields",
|
||||
"snackbarViewQueue": "View Queue",
|
||||
|
||||
"errorRateLimited": "Rate Limited",
|
||||
"errorRateLimitedMessage": "Too many requests. Please wait a moment before searching again.",
|
||||
"errorFailedToLoad": "Failed to load {item}",
|
||||
"@errorFailedToLoad": {
|
||||
"placeholders": {
|
||||
"item": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"errorNoTracksFound": "No tracks found",
|
||||
"errorMissingExtensionSource": "Cannot load {item}: missing extension source",
|
||||
"@errorMissingExtensionSource": {
|
||||
"placeholders": {
|
||||
"item": {"type": "String"}
|
||||
}
|
||||
},
|
||||
|
||||
"statusQueued": "Queued",
|
||||
"statusDownloading": "Downloading",
|
||||
"statusFinalizing": "Finalizing",
|
||||
"statusCompleted": "Completed",
|
||||
"statusFailed": "Failed",
|
||||
"statusSkipped": "Skipped",
|
||||
"statusPaused": "Paused",
|
||||
|
||||
"actionPause": "Pause",
|
||||
"actionResume": "Resume",
|
||||
"actionCancel": "Cancel",
|
||||
"actionStop": "Stop",
|
||||
"actionSelect": "Select",
|
||||
"actionSelectAll": "Select All",
|
||||
"actionDeselect": "Deselect",
|
||||
"actionPaste": "Paste",
|
||||
"actionImportCsv": "Import CSV",
|
||||
"actionRemoveCredentials": "Remove Credentials",
|
||||
"actionSaveCredentials": "Save Credentials",
|
||||
|
||||
"selectionSelected": "{count} selected",
|
||||
"@selectionSelected": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"selectionAllSelected": "All tracks selected",
|
||||
"selectionTapToSelect": "Tap tracks to select",
|
||||
"selectionDeleteTracks": "Delete {count} {count, plural, =1{track} other{tracks}}",
|
||||
"@selectionDeleteTracks": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"selectionSelectToDelete": "Select tracks to delete",
|
||||
|
||||
"progressFetchingMetadata": "Fetching metadata... {current}/{total}",
|
||||
"@progressFetchingMetadata": {
|
||||
"placeholders": {
|
||||
"current": {"type": "int"},
|
||||
"total": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"progressReadingCsv": "Reading CSV...",
|
||||
|
||||
"searchSongs": "Songs",
|
||||
"searchArtists": "Artists",
|
||||
"searchAlbums": "Albums",
|
||||
"searchPlaylists": "Playlists",
|
||||
|
||||
"tooltipPlay": "Play",
|
||||
"tooltipCancel": "Cancel",
|
||||
"tooltipStop": "Stop",
|
||||
"tooltipRetry": "Retry",
|
||||
"tooltipRemove": "Remove",
|
||||
"tooltipClear": "Clear",
|
||||
"tooltipPaste": "Paste",
|
||||
|
||||
"filenameFormat": "Filename Format",
|
||||
"filenameFormatPreview": "Preview: {preview}",
|
||||
"@filenameFormatPreview": {
|
||||
"placeholders": {
|
||||
"preview": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"folderOrganization": "Folder Organization",
|
||||
"folderOrganizationNone": "No organization",
|
||||
"folderOrganizationByArtist": "By Artist",
|
||||
"folderOrganizationByAlbum": "By Album",
|
||||
"folderOrganizationByArtistAlbum": "Artist/Album",
|
||||
|
||||
"updateAvailable": "Update Available",
|
||||
"updateNewVersion": "Version {version} is available",
|
||||
"@updateNewVersion": {
|
||||
"placeholders": {
|
||||
"version": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"updateDownload": "Download",
|
||||
"updateLater": "Later",
|
||||
"updateChangelog": "Changelog",
|
||||
|
||||
"providerPriority": "Provider Priority",
|
||||
"providerPrioritySubtitle": "Drag to reorder download providers",
|
||||
"metadataProviderPriority": "Metadata Provider Priority",
|
||||
"metadataProviderPrioritySubtitle": "Order used when fetching track metadata",
|
||||
|
||||
"logTitle": "Logs",
|
||||
"logCopy": "Copy Logs",
|
||||
"logClear": "Clear Logs",
|
||||
"logShare": "Share Logs",
|
||||
"logEmpty": "No logs yet",
|
||||
"logCopied": "Logs copied to clipboard",
|
||||
|
||||
"credentialsTitle": "Spotify Credentials",
|
||||
"credentialsDescription": "Enter your Client ID and Secret to use your own Spotify application quota.",
|
||||
"credentialsClientId": "Client ID",
|
||||
"credentialsClientIdHint": "Paste Client ID",
|
||||
"credentialsClientSecret": "Client Secret",
|
||||
"credentialsClientSecretHint": "Paste Client Secret",
|
||||
|
||||
"channelStable": "Stable",
|
||||
"channelPreview": "Preview",
|
||||
|
||||
"sectionSearchSource": "Search Source",
|
||||
"sectionDownload": "Download",
|
||||
"sectionPerformance": "Performance",
|
||||
"sectionApp": "App",
|
||||
"sectionData": "Data",
|
||||
"sectionDebug": "Debug",
|
||||
"sectionService": "Service",
|
||||
"sectionAudioQuality": "Audio Quality",
|
||||
"sectionFileSettings": "File Settings",
|
||||
"sectionColor": "Color",
|
||||
"sectionTheme": "Theme",
|
||||
"sectionLayout": "Layout",
|
||||
|
||||
"settingsAppearanceSubtitle": "Theme, colors, display",
|
||||
"settingsDownloadSubtitle": "Service, quality, filename format",
|
||||
"settingsOptionsSubtitle": "Fallback, lyrics, cover art, updates",
|
||||
"settingsExtensionsSubtitle": "Manage download providers",
|
||||
"settingsLogsSubtitle": "View app logs for debugging",
|
||||
|
||||
"loadingSharedLink": "Loading shared link...",
|
||||
"pressBackAgainToExit": "Press back again to exit",
|
||||
|
||||
"artistReleases": "{count, plural, =1{1 release} other{{count} releases}}",
|
||||
"@artistReleases": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"artistCompilations": "Compilations",
|
||||
|
||||
"tracksHeader": "Tracks",
|
||||
"downloadAllCount": "Download All ({count})",
|
||||
"@downloadAllCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"tracksCount": "{count, plural, =1{1 track} other{{count} tracks}}",
|
||||
"@tracksCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
|
||||
"setupStorageAccessRequired": "Storage Access Required",
|
||||
"setupStorageAccessMessage": "SpotiFLAC needs \"All files access\" permission to save music files to your chosen folder.",
|
||||
"setupStorageAccessMessageAndroid11": "Android 11+ requires \"All files access\" permission to save files to your chosen download folder.",
|
||||
"setupOpenSettings": "Open Settings",
|
||||
"setupPermissionDeniedMessage": "Permission denied. Please grant all permissions to continue.",
|
||||
"setupPermissionRequired": "{permissionType} Permission Required",
|
||||
"@setupPermissionRequired": {
|
||||
"placeholders": {
|
||||
"permissionType": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"setupPermissionRequiredMessage": "{permissionType} permission is required for the best experience. You can change this later in Settings.",
|
||||
"@setupPermissionRequiredMessage": {
|
||||
"placeholders": {
|
||||
"permissionType": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"setupSelectDownloadFolder": "Select Download Folder",
|
||||
"setupUseDefaultFolder": "Use Default Folder?",
|
||||
"setupNoFolderSelected": "No folder selected. Would you like to use the default Music folder?",
|
||||
"setupUseDefault": "Use Default",
|
||||
"setupDownloadLocationTitle": "Download Location",
|
||||
"setupDownloadLocationIosMessage": "On iOS, downloads are saved to the app's Documents folder. You can access them via the Files app.",
|
||||
"setupAppDocumentsFolder": "App Documents Folder",
|
||||
"setupAppDocumentsFolderSubtitle": "Recommended - accessible via Files app",
|
||||
"setupChooseFromFiles": "Choose from Files",
|
||||
"setupChooseFromFilesSubtitle": "Select iCloud or other location",
|
||||
"setupIosEmptyFolderWarning": "iOS limitation: Empty folders cannot be selected. Choose a folder with at least one file.",
|
||||
"setupDownloadInFlac": "Download Spotify tracks in FLAC",
|
||||
"setupStepStorage": "Storage",
|
||||
"setupStepNotification": "Notification",
|
||||
"setupStepFolder": "Folder",
|
||||
"setupStepSpotify": "Spotify",
|
||||
"setupStepPermission": "Permission",
|
||||
"setupStorageGranted": "Storage Permission Granted!",
|
||||
"setupStorageRequired": "Storage Permission Required",
|
||||
"setupStorageDescription": "SpotiFLAC needs storage permission to save your downloaded music files.",
|
||||
"setupNotificationGranted": "Notification Permission Granted!",
|
||||
"setupNotificationEnable": "Enable Notifications",
|
||||
"setupNotificationDescription": "Get notified when downloads complete or require attention.",
|
||||
"setupFolderSelected": "Download Folder Selected!",
|
||||
"setupFolderChoose": "Choose Download Folder",
|
||||
"setupFolderDescription": "Select a folder where your downloaded music will be saved.",
|
||||
"setupChangeFolder": "Change Folder",
|
||||
"setupSelectFolder": "Select Folder",
|
||||
"setupSpotifyApiOptional": "Spotify API (Optional)",
|
||||
"setupSpotifyApiDescription": "Add your Spotify API credentials for better search results and access to Spotify-exclusive content.",
|
||||
"setupUseSpotifyApi": "Use Spotify API",
|
||||
"setupEnterCredentialsBelow": "Enter your credentials below",
|
||||
"setupUsingDeezer": "Using Deezer (no account needed)",
|
||||
"setupEnterClientId": "Enter Spotify Client ID",
|
||||
"setupEnterClientSecret": "Enter Spotify Client Secret",
|
||||
"setupGetFreeCredentials": "Get your free API credentials from the Spotify Developer Dashboard.",
|
||||
"setupEnableNotifications": "Enable Notifications",
|
||||
|
||||
"dialogImport": "Import",
|
||||
"dialogDiscard": "Discard",
|
||||
"dialogRemove": "Remove",
|
||||
"dialogUninstall": "Uninstall",
|
||||
"dialogDiscardChanges": "Discard Changes?",
|
||||
"dialogUnsavedChanges": "You have unsaved changes. Do you want to discard them?",
|
||||
"dialogDownloadFailed": "Download Failed",
|
||||
"dialogTrackLabel": "Track:",
|
||||
"dialogArtistLabel": "Artist:",
|
||||
"dialogErrorLabel": "Error:",
|
||||
"dialogClearAll": "Clear All",
|
||||
"dialogClearAllDownloads": "Are you sure you want to clear all downloads?",
|
||||
"dialogRemoveFromDevice": "Remove from device?",
|
||||
"dialogRemoveExtension": "Remove Extension",
|
||||
"dialogRemoveExtensionMessage": "Are you sure you want to remove this extension? This cannot be undone.",
|
||||
"dialogUninstallExtension": "Uninstall Extension?",
|
||||
"dialogUninstallExtensionMessage": "Are you sure you want to remove {extensionName}?",
|
||||
"@dialogUninstallExtensionMessage": {
|
||||
"placeholders": {
|
||||
"extensionName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
|
||||
"snackbarFailedToLoad": "Failed to load: {error}",
|
||||
"@snackbarFailedToLoad": {
|
||||
"placeholders": {
|
||||
"error": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"snackbarUrlCopied": "{platform} URL copied to clipboard",
|
||||
"@snackbarUrlCopied": {
|
||||
"placeholders": {
|
||||
"platform": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"snackbarFileNotFound": "File not found",
|
||||
"snackbarSelectExtFile": "Please select a .spotiflac-ext file",
|
||||
"snackbarProviderPrioritySaved": "Provider priority saved",
|
||||
"snackbarMetadataProviderSaved": "Metadata provider priority saved",
|
||||
"snackbarExtensionInstalled": "{extensionName} installed.",
|
||||
"@snackbarExtensionInstalled": {
|
||||
"placeholders": {
|
||||
"extensionName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"snackbarExtensionUpdated": "{extensionName} updated.",
|
||||
"@snackbarExtensionUpdated": {
|
||||
"placeholders": {
|
||||
"extensionName": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"snackbarFailedToInstall": "Failed to install extension",
|
||||
"snackbarFailedToUpdate": "Failed to update extension",
|
||||
|
||||
"storeFilterAll": "All",
|
||||
"storeFilterMetadata": "Metadata",
|
||||
"storeFilterDownload": "Download",
|
||||
"storeFilterUtility": "Utility",
|
||||
"storeFilterLyrics": "Lyrics",
|
||||
"storeFilterIntegration": "Integration",
|
||||
"storeClearFilters": "Clear filters",
|
||||
"storeNoResults": "No extensions found",
|
||||
|
||||
"extensionProviderPriority": "Provider Priority",
|
||||
"extensionInstallButton": "Install Extension",
|
||||
"extensionDefaultProvider": "Default (Deezer/Spotify)",
|
||||
"extensionDefaultProviderSubtitle": "Use built-in search",
|
||||
"extensionAuthor": "Author",
|
||||
"extensionId": "ID",
|
||||
"extensionError": "Error",
|
||||
"extensionCapabilities": "Capabilities",
|
||||
"extensionMetadataProvider": "Metadata Provider",
|
||||
"extensionDownloadProvider": "Download Provider",
|
||||
"extensionLyricsProvider": "Lyrics Provider",
|
||||
"extensionUrlHandler": "URL Handler",
|
||||
"extensionQualityOptions": "Quality Options",
|
||||
"extensionPostProcessingHooks": "Post-Processing Hooks",
|
||||
"extensionPermissions": "Permissions",
|
||||
"extensionSettings": "Settings",
|
||||
"extensionRemoveButton": "Remove Extension",
|
||||
"extensionUpdated": "Updated",
|
||||
"extensionMinAppVersion": "Min App Version",
|
||||
|
||||
"qualityFlacLossless": "FLAC Lossless",
|
||||
"qualityFlacLosslessSubtitle": "16-bit / 44.1kHz",
|
||||
"qualityHiResFlac": "Hi-Res FLAC",
|
||||
"qualityHiResFlacSubtitle": "24-bit / up to 96kHz",
|
||||
"qualityHiResFlacMax": "Hi-Res FLAC Max",
|
||||
"qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz",
|
||||
"qualityNote": "Actual quality depends on track availability from the service",
|
||||
|
||||
"downloadAskBeforeDownload": "Ask Before Download",
|
||||
"downloadDirectory": "Download Directory",
|
||||
"downloadSeparateSinglesFolder": "Separate Singles Folder",
|
||||
"downloadAlbumFolderStructure": "Album Folder Structure",
|
||||
"downloadSaveFormat": "Save Format",
|
||||
"downloadSelectService": "Select Service",
|
||||
"downloadSelectQuality": "Select Quality",
|
||||
"downloadFrom": "Download From",
|
||||
"downloadDefaultQualityLabel": "Default Quality",
|
||||
"downloadBestAvailable": "Best available",
|
||||
|
||||
"folderNone": "None",
|
||||
"folderNoneSubtitle": "Save all files directly to download folder",
|
||||
"folderArtist": "Artist",
|
||||
"folderArtistSubtitle": "Artist Name/filename",
|
||||
"folderAlbum": "Album",
|
||||
"folderAlbumSubtitle": "Album Name/filename",
|
||||
"folderArtistAlbum": "Artist/Album",
|
||||
"folderArtistAlbumSubtitle": "Artist Name/Album Name/filename",
|
||||
|
||||
"serviceTidal": "Tidal",
|
||||
"serviceQobuz": "Qobuz",
|
||||
"serviceAmazon": "Amazon",
|
||||
"serviceDeezer": "Deezer",
|
||||
"serviceSpotify": "Spotify",
|
||||
|
||||
"logSearchHint": "Search logs...",
|
||||
"logFilterLevel": "Level",
|
||||
"logFilterSection": "Filter",
|
||||
"logShareLogs": "Share logs",
|
||||
"logClearLogs": "Clear logs",
|
||||
"logClearLogsTitle": "Clear Logs",
|
||||
"logClearLogsMessage": "Are you sure you want to clear all logs?",
|
||||
"logIspBlocking": "ISP BLOCKING DETECTED",
|
||||
"logRateLimited": "RATE LIMITED",
|
||||
"logNetworkError": "NETWORK ERROR",
|
||||
"logTrackNotFound": "TRACK NOT FOUND",
|
||||
|
||||
"appearanceAmoledDark": "AMOLED Dark",
|
||||
"appearanceAmoledDarkSubtitle": "Pure black background",
|
||||
"appearanceChooseAccentColor": "Choose Accent Color",
|
||||
"appearanceChooseTheme": "Theme Mode",
|
||||
|
||||
"updateStartingDownload": "Starting download...",
|
||||
"updateDownloadFailed": "Download failed",
|
||||
"updateFailedMessage": "Failed to download update",
|
||||
"updateNewVersionReady": "A new version is ready",
|
||||
"updateCurrent": "Current",
|
||||
"updateNew": "New",
|
||||
"updateDownloading": "Downloading...",
|
||||
"updateWhatsNew": "What's New",
|
||||
"updateDownloadInstall": "Download & Install",
|
||||
"updateDontRemind": "Don't remind",
|
||||
|
||||
"trackCopyFilePath": "Copy file path",
|
||||
"trackRemoveFromDevice": "Remove from device",
|
||||
"trackLoadLyrics": "Load Lyrics",
|
||||
|
||||
"dateToday": "Today",
|
||||
"dateYesterday": "Yesterday",
|
||||
"dateDaysAgo": "{count} days ago",
|
||||
"@dateDaysAgo": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"dateWeeksAgo": "{count} weeks ago",
|
||||
"@dateWeeksAgo": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"dateMonthsAgo": "{count} months ago",
|
||||
"@dateMonthsAgo": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
|
||||
"concurrentSequential": "Sequential",
|
||||
"concurrentParallel2": "2 Parallel",
|
||||
"concurrentParallel3": "3 Parallel",
|
||||
|
||||
"filenameAvailablePlaceholders": "Available placeholders:",
|
||||
"filenameHint": "{artist} - {title}",
|
||||
|
||||
"tapToSeeError": "Tap to see error details",
|
||||
|
||||
"setupProceedToNextStep": "You can now proceed to the next step.",
|
||||
"setupNotificationProgressDescription": "You will receive download progress notifications.",
|
||||
"setupNotificationBackgroundDescription": "Get notified about download progress and completion. This helps you track downloads when the app is in background.",
|
||||
"setupSkipForNow": "Skip for now",
|
||||
"setupBack": "Back",
|
||||
"setupNext": "Next",
|
||||
"setupGetStarted": "Get Started",
|
||||
"setupSkipAndStart": "Skip & Start",
|
||||
"setupAllowAccessToManageFiles": "Please enable \"Allow access to manage all files\" in the next screen.",
|
||||
"setupGetCredentialsFromSpotify": "Get credentials from developer.spotify.com",
|
||||
|
||||
"trackMetadata": "Metadata",
|
||||
"trackFileInfo": "File Info",
|
||||
"trackLyrics": "Lyrics",
|
||||
"trackFileNotFound": "File not found",
|
||||
"trackOpenInDeezer": "Open in Deezer",
|
||||
"trackOpenInSpotify": "Open in Spotify",
|
||||
"trackTrackName": "Track name",
|
||||
"trackArtist": "Artist",
|
||||
"trackAlbumArtist": "Album artist",
|
||||
"trackAlbum": "Album",
|
||||
"trackTrackNumber": "Track number",
|
||||
"trackDiscNumber": "Disc number",
|
||||
"trackDuration": "Duration",
|
||||
"trackAudioQuality": "Audio quality",
|
||||
"trackReleaseDate": "Release date",
|
||||
"trackDownloaded": "Downloaded",
|
||||
"trackCopyLyrics": "Copy lyrics",
|
||||
"trackLyricsNotAvailable": "Lyrics not available for this track",
|
||||
"trackLyricsTimeout": "Request timed out. Try again later.",
|
||||
"trackLyricsLoadFailed": "Failed to load lyrics",
|
||||
"trackCopiedToClipboard": "Copied to clipboard",
|
||||
"trackDeleteConfirmTitle": "Remove from device?",
|
||||
"trackDeleteConfirmMessage": "This will permanently delete the downloaded file and remove it from your history.",
|
||||
"trackCannotOpen": "Cannot open: {message}",
|
||||
"@trackCannotOpen": {
|
||||
"placeholders": {
|
||||
"message": {"type": "String"}
|
||||
}
|
||||
},
|
||||
|
||||
"logFilterBySeverity": "Filter logs by severity",
|
||||
"logNoLogsYet": "No logs yet",
|
||||
"logNoLogsYetSubtitle": "Logs will appear here as you use the app",
|
||||
"logIssueSummary": "Issue Summary",
|
||||
"logIspBlockingDescription": "Your ISP may be blocking access to download services",
|
||||
"logIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8",
|
||||
"logRateLimitedDescription": "Too many requests to the service",
|
||||
"logRateLimitedSuggestion": "Wait a few minutes before trying again",
|
||||
"logNetworkErrorDescription": "Connection issues detected",
|
||||
"logNetworkErrorSuggestion": "Check your internet connection",
|
||||
"logTrackNotFoundDescription": "Some tracks could not be found on download services",
|
||||
"logTrackNotFoundSuggestion": "The track may not be available in lossless quality",
|
||||
"logTotalErrors": "Total errors: {count}",
|
||||
"@logTotalErrors": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"logAffected": "Affected: {domains}",
|
||||
"@logAffected": {
|
||||
"placeholders": {
|
||||
"domains": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"logEntriesFiltered": "Entries ({count} filtered)",
|
||||
"@logEntriesFiltered": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"logEntries": "Entries ({count})",
|
||||
"@logEntries": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
|
||||
"extensionsProviderPrioritySection": "Provider Priority",
|
||||
"extensionsInstalledSection": "Installed Extensions",
|
||||
"extensionsNoExtensions": "No extensions installed",
|
||||
"extensionsNoExtensionsSubtitle": "Install .spotiflac-ext files to add new providers",
|
||||
"extensionsInstallButton": "Install Extension",
|
||||
"extensionsInfoTip": "Extensions can add new metadata and download providers. Only install extensions from trusted sources.",
|
||||
"extensionsInstalledSuccess": "Extension installed successfully",
|
||||
"extensionsDownloadPriority": "Download Priority",
|
||||
"extensionsDownloadPrioritySubtitle": "Set download service order",
|
||||
"extensionsNoDownloadProvider": "No extensions with download provider",
|
||||
"extensionsMetadataPriority": "Metadata Priority",
|
||||
"extensionsMetadataPrioritySubtitle": "Set search & metadata source order",
|
||||
"extensionsNoMetadataProvider": "No extensions with metadata provider",
|
||||
"extensionsSearchProvider": "Search Provider",
|
||||
"extensionsNoCustomSearch": "No extensions with custom search",
|
||||
"extensionsSearchProviderDescription": "Choose which service to use for searching tracks",
|
||||
"extensionsCustomSearch": "Custom search",
|
||||
"extensionsErrorLoading": "Error loading extension",
|
||||
|
||||
"extensionCustomTrackMatching": "Custom Track Matching",
|
||||
"extensionPostProcessing": "Post-Processing",
|
||||
"extensionHooksAvailable": "{count} hook(s) available",
|
||||
"@extensionHooksAvailable": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"extensionPatternsCount": "{count} pattern(s)",
|
||||
"@extensionPatternsCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"extensionStrategy": "Strategy: {strategy}",
|
||||
"@extensionStrategy": {
|
||||
"placeholders": {
|
||||
"strategy": {"type": "String"}
|
||||
}
|
||||
},
|
||||
|
||||
"aboutDoubleDouble": "DoubleDouble",
|
||||
"aboutDoubleDoubleDesc": "Amazing API for Amazon Music downloads. Thank you for making it free!",
|
||||
"aboutDabMusic": "DAB Music",
|
||||
"aboutDabMusicDesc": "The best Qobuz streaming API. Hi-Res downloads wouldn't be possible without this!",
|
||||
|
||||
"queueTitle": "Download Queue",
|
||||
"queueClearAll": "Clear All",
|
||||
"queueClearAllMessage": "Are you sure you want to clear all downloads?",
|
||||
|
||||
"albumFolderArtistAlbum": "Artist / Album",
|
||||
"albumFolderArtistAlbumSubtitle": "Albums/Artist Name/Album Name/",
|
||||
"albumFolderArtistYearAlbum": "Artist / [Year] Album",
|
||||
"albumFolderArtistYearAlbumSubtitle": "Albums/Artist Name/[2005] Album Name/",
|
||||
"albumFolderAlbumOnly": "Album Only",
|
||||
"albumFolderAlbumOnlySubtitle": "Albums/Album Name/",
|
||||
"albumFolderYearAlbum": "[Year] Album",
|
||||
"albumFolderYearAlbumSubtitle": "Albums/[2005] Album Name/",
|
||||
|
||||
"downloadedAlbumDeleteSelected": "Delete Selected",
|
||||
"downloadedAlbumDeleteMessage": "Delete {count} {count, plural, =1{track} other{tracks}} from this album?\n\nThis will also delete the files from storage.",
|
||||
"@downloadedAlbumDeleteMessage": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
|
||||
"utilityFunctions": "Utility Functions",
|
||||
|
||||
"aboutMobileDeveloper": "Mobile version developer",
|
||||
"aboutOriginalCreator": "Creator of the original SpotiFLAC",
|
||||
"aboutLogoArtist": "The talented artist who created our beautiful app logo!",
|
||||
"aboutBinimumDesc": "The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn't exist!",
|
||||
"aboutSachinsenalDesc": "The original HiFi project creator. The foundation of Tidal integration!",
|
||||
"aboutMobileSource": "Mobile source code",
|
||||
"aboutPCSource": "PC source code",
|
||||
"aboutReportIssue": "Report an issue",
|
||||
"aboutReportIssueSubtitle": "Report any problems you encounter",
|
||||
"aboutFeatureRequest": "Feature request",
|
||||
"aboutFeatureRequestSubtitle": "Suggest new features for the app",
|
||||
"aboutBuyMeCoffee": "Buy me a coffee",
|
||||
"aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi",
|
||||
"aboutVersion": "Version",
|
||||
"aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
|
||||
"providerPriorityTitle": "Provider Priority",
|
||||
"providerPriorityDescription": "Drag to reorder download providers. The app will try providers from top to bottom when downloading tracks.",
|
||||
"providerPriorityInfo": "If a track is not available on the first provider, the app will automatically try the next one.",
|
||||
"providerBuiltIn": "Built-in",
|
||||
"providerExtension": "Extension",
|
||||
|
||||
"metadataProviderPriorityTitle": "Metadata Priority",
|
||||
"metadataProviderPriorityDescription": "Drag to reorder metadata providers. The app will try providers from top to bottom when searching for tracks and fetching metadata.",
|
||||
"metadataProviderPriorityInfo": "Deezer has no rate limits and is recommended as primary. Spotify may rate limit after many requests.",
|
||||
"metadataNoRateLimits": "No rate limits",
|
||||
"metadataMayRateLimit": "May rate limit",
|
||||
|
||||
"queueEmpty": "No downloads in queue",
|
||||
"queueEmptySubtitle": "Add tracks from the home screen",
|
||||
"queueClearCompleted": "Clear completed",
|
||||
"queueDownloadFailed": "Download Failed",
|
||||
"queueTrackLabel": "Track:",
|
||||
"queueArtistLabel": "Artist:",
|
||||
"queueErrorLabel": "Error:",
|
||||
"queueUnknownError": "Unknown error",
|
||||
|
||||
"downloadedAlbumTracksHeader": "Tracks",
|
||||
"downloadedAlbumDownloadedCount": "{count} downloaded",
|
||||
"@downloadedAlbumDownloadedCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"downloadedAlbumSelectedCount": "{count} selected",
|
||||
"@downloadedAlbumSelectedCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"downloadedAlbumAllSelected": "All tracks selected",
|
||||
"downloadedAlbumTapToSelect": "Tap tracks to select",
|
||||
"downloadedAlbumDeleteCount": "Delete {count} {count, plural, =1{track} other{tracks}}",
|
||||
"@downloadedAlbumDeleteCount": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"downloadedAlbumSelectToDelete": "Select tracks to delete",
|
||||
|
||||
"folderOrganizationDescription": "Organize downloaded files into folders",
|
||||
"folderOrganizationNone": "None",
|
||||
"folderOrganizationNoneSubtitle": "All files in download folder",
|
||||
"folderOrganizationByArtist": "By Artist",
|
||||
"folderOrganizationByArtistSubtitle": "Separate folder for each artist",
|
||||
"folderOrganizationByAlbum": "By Album",
|
||||
"folderOrganizationByAlbumSubtitle": "Separate folder for each album",
|
||||
"folderOrganizationByArtistAlbum": "By Artist & Album",
|
||||
"folderOrganizationByArtistAlbumSubtitle": "Nested folders for artist and album"
|
||||
}
|
||||
664
lib/l10n/arb/app_id.arb
Normal file
664
lib/l10n/arb/app_id.arb
Normal file
|
|
@ -0,0 +1,664 @@
|
|||
{
|
||||
"@@locale": "id",
|
||||
"@@last_modified": "2026-01-16",
|
||||
|
||||
"appName": "SpotiFLAC",
|
||||
"appDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.",
|
||||
|
||||
"navHome": "Beranda",
|
||||
"navHistory": "Riwayat",
|
||||
"navSettings": "Pengaturan",
|
||||
"navStore": "Toko",
|
||||
|
||||
"homeTitle": "Beranda",
|
||||
"homeSearchHint": "Tempel URL Spotify atau cari...",
|
||||
"homeSearchHintExtension": "Cari dengan {extensionName}...",
|
||||
"homeSubtitle": "Tempel link Spotify atau cari berdasarkan nama",
|
||||
"homeSupports": "Mendukung: URL Track, Album, Playlist, Artis",
|
||||
"homeRecent": "Terbaru",
|
||||
|
||||
"historyTitle": "Riwayat",
|
||||
"historyDownloading": "Mengunduh ({count})",
|
||||
"historyDownloaded": "Terunduh",
|
||||
"historyFilterAll": "Semua",
|
||||
"historyFilterAlbums": "Album",
|
||||
"historyFilterSingles": "Single",
|
||||
"historyTracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}",
|
||||
"historyAlbumsCount": "{count, plural, =1{1 album} other{{count} album}}",
|
||||
"historyNoDownloads": "Tidak ada riwayat unduhan",
|
||||
"historyNoDownloadsSubtitle": "Lagu yang diunduh akan muncul di sini",
|
||||
"historyNoAlbums": "Tidak ada unduhan album",
|
||||
"historyNoAlbumsSubtitle": "Unduh beberapa lagu dari album untuk melihatnya di sini",
|
||||
"historyNoSingles": "Tidak ada unduhan single",
|
||||
"historyNoSinglesSubtitle": "Unduhan lagu satuan akan muncul di sini",
|
||||
|
||||
"settingsTitle": "Pengaturan",
|
||||
"settingsDownload": "Unduhan",
|
||||
"settingsAppearance": "Tampilan",
|
||||
"settingsOptions": "Opsi",
|
||||
"settingsExtensions": "Ekstensi",
|
||||
"settingsAbout": "Tentang",
|
||||
|
||||
"downloadTitle": "Unduhan",
|
||||
"downloadLocation": "Lokasi Unduhan",
|
||||
"downloadLocationSubtitle": "Pilih tempat menyimpan file",
|
||||
"downloadLocationDefault": "Lokasi default",
|
||||
"downloadDefaultService": "Layanan Default",
|
||||
"downloadDefaultServiceSubtitle": "Layanan yang digunakan untuk unduhan",
|
||||
"downloadDefaultQuality": "Kualitas Default",
|
||||
"downloadAskQuality": "Tanya Kualitas Sebelum Unduh",
|
||||
"downloadAskQualitySubtitle": "Tampilkan pemilih kualitas untuk setiap unduhan",
|
||||
"downloadFilenameFormat": "Format Nama File",
|
||||
"downloadFolderOrganization": "Organisasi Folder",
|
||||
"downloadSeparateSingles": "Pisahkan Single",
|
||||
"downloadSeparateSinglesSubtitle": "Letakkan lagu satuan di folder terpisah",
|
||||
|
||||
"qualityBest": "Terbaik",
|
||||
"qualityFlac": "FLAC",
|
||||
"quality320": "320 kbps",
|
||||
"quality128": "128 kbps",
|
||||
|
||||
"appearanceTitle": "Tampilan",
|
||||
"appearanceTheme": "Tema",
|
||||
"appearanceThemeSystem": "Sistem",
|
||||
"appearanceThemeLight": "Terang",
|
||||
"appearanceThemeDark": "Gelap",
|
||||
"appearanceDynamicColor": "Warna Dinamis",
|
||||
"appearanceDynamicColorSubtitle": "Gunakan warna dari wallpaper Anda",
|
||||
"appearanceAccentColor": "Warna Aksen",
|
||||
"appearanceHistoryView": "Tampilan Riwayat",
|
||||
"appearanceHistoryViewList": "Daftar",
|
||||
"appearanceHistoryViewGrid": "Grid",
|
||||
|
||||
"optionsTitle": "Opsi",
|
||||
"optionsSearchSource": "Sumber Pencarian",
|
||||
"optionsPrimaryProvider": "Provider Utama",
|
||||
"optionsPrimaryProviderSubtitle": "Layanan yang digunakan saat mencari berdasarkan nama lagu.",
|
||||
"optionsUsingExtension": "Menggunakan ekstensi: {extensionName}",
|
||||
"optionsSwitchBack": "Ketuk Deezer atau Spotify untuk beralih dari ekstensi",
|
||||
"optionsAutoFallback": "Auto Fallback",
|
||||
"optionsAutoFallbackSubtitle": "Coba layanan lain jika unduhan gagal",
|
||||
"optionsUseExtensionProviders": "Gunakan Provider Ekstensi",
|
||||
"optionsUseExtensionProvidersOn": "Ekstensi akan dicoba terlebih dahulu",
|
||||
"optionsUseExtensionProvidersOff": "Hanya menggunakan provider bawaan",
|
||||
"optionsEmbedLyrics": "Sematkan Lirik",
|
||||
"optionsEmbedLyricsSubtitle": "Sematkan lirik sinkron ke file FLAC",
|
||||
"optionsMaxQualityCover": "Cover Kualitas Maksimal",
|
||||
"optionsMaxQualityCoverSubtitle": "Unduh cover art resolusi tertinggi",
|
||||
"optionsConcurrentDownloads": "Unduhan Bersamaan",
|
||||
"optionsConcurrentSequential": "Berurutan (1 per waktu)",
|
||||
"optionsConcurrentParallel": "{count} unduhan paralel",
|
||||
"optionsConcurrentWarning": "Unduhan paralel dapat memicu pembatasan rate",
|
||||
"optionsExtensionStore": "Toko Ekstensi",
|
||||
"optionsExtensionStoreSubtitle": "Tampilkan tab Toko di navigasi",
|
||||
"optionsCheckUpdates": "Periksa Pembaruan",
|
||||
"optionsCheckUpdatesSubtitle": "Beritahu saat versi baru tersedia",
|
||||
"optionsUpdateChannel": "Saluran Pembaruan",
|
||||
"optionsUpdateChannelStable": "Hanya rilis stabil",
|
||||
"optionsUpdateChannelPreview": "Dapatkan rilis preview",
|
||||
"optionsUpdateChannelWarning": "Preview mungkin mengandung bug atau fitur belum lengkap",
|
||||
"optionsClearHistory": "Hapus Riwayat Unduhan",
|
||||
"optionsClearHistorySubtitle": "Hapus semua lagu dari riwayat",
|
||||
"optionsDetailedLogging": "Log Detail",
|
||||
"optionsDetailedLoggingOn": "Log detail sedang direkam",
|
||||
"optionsDetailedLoggingOff": "Aktifkan untuk laporan bug",
|
||||
"optionsSpotifyCredentials": "Kredensial Spotify",
|
||||
"optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...",
|
||||
"optionsSpotifyCredentialsRequired": "Diperlukan - ketuk untuk mengatur",
|
||||
"optionsSpotifyWarning": "Spotify memerlukan kredensial API Anda sendiri. Dapatkan gratis dari developer.spotify.com",
|
||||
|
||||
"extensionsTitle": "Ekstensi",
|
||||
"extensionsInstalled": "Ekstensi Terpasang",
|
||||
"extensionsNone": "Tidak ada ekstensi terpasang",
|
||||
"extensionsNoneSubtitle": "Pasang ekstensi dari tab Toko",
|
||||
"extensionsEnabled": "Aktif",
|
||||
"extensionsDisabled": "Nonaktif",
|
||||
"extensionsVersion": "Versi {version}",
|
||||
"extensionsAuthor": "oleh {author}",
|
||||
"extensionsUninstall": "Copot",
|
||||
"extensionsSetAsSearch": "Jadikan Provider Pencarian",
|
||||
|
||||
"storeTitle": "Toko Ekstensi",
|
||||
"storeSearch": "Cari ekstensi...",
|
||||
"storeInstall": "Pasang",
|
||||
"storeInstalled": "Terpasang",
|
||||
"storeUpdate": "Perbarui",
|
||||
|
||||
"aboutTitle": "Tentang",
|
||||
"aboutContributors": "Kontributor",
|
||||
"aboutMobileDeveloper": "Pengembang versi mobile",
|
||||
"aboutOriginalCreator": "Pencipta SpotiFLAC asli",
|
||||
"aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kami yang indah!",
|
||||
"aboutSpecialThanks": "Terima Kasih Khusus",
|
||||
"aboutLinks": "Tautan",
|
||||
"aboutMobileSource": "Kode sumber mobile",
|
||||
"aboutPCSource": "Kode sumber PC",
|
||||
"aboutReportIssue": "Laporkan masalah",
|
||||
"aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui",
|
||||
"aboutFeatureRequest": "Permintaan fitur",
|
||||
"aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi",
|
||||
"aboutSupport": "Dukungan",
|
||||
"aboutBuyMeCoffee": "Traktir saya kopi",
|
||||
"aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi",
|
||||
"aboutApp": "Aplikasi",
|
||||
"aboutVersion": "Versi",
|
||||
|
||||
"albumTitle": "Album",
|
||||
"albumTracks": "{count, plural, =1{1 lagu} other{{count} lagu}}",
|
||||
"albumDownloadAll": "Unduh Semua",
|
||||
"albumDownloadRemaining": "Unduh Sisanya",
|
||||
|
||||
"playlistTitle": "Playlist",
|
||||
"artistTitle": "Artis",
|
||||
"artistAlbums": "Album",
|
||||
"artistSingles": "Single & EP",
|
||||
|
||||
"trackMetadataTitle": "Info Lagu",
|
||||
"trackMetadataArtist": "Artis",
|
||||
"trackMetadataAlbum": "Album",
|
||||
"trackMetadataDuration": "Durasi",
|
||||
"trackMetadataQuality": "Kualitas",
|
||||
"trackMetadataPath": "Lokasi File",
|
||||
"trackMetadataDownloadedAt": "Diunduh",
|
||||
"trackMetadataService": "Layanan",
|
||||
"trackMetadataPlay": "Putar",
|
||||
"trackMetadataShare": "Bagikan",
|
||||
"trackMetadataDelete": "Hapus",
|
||||
"trackMetadataRedownload": "Unduh ulang",
|
||||
"trackMetadataOpenFolder": "Buka Folder",
|
||||
|
||||
"setupTitle": "Selamat Datang di SpotiFLAC",
|
||||
"setupSubtitle": "Mari mulai pengaturan",
|
||||
"setupStoragePermission": "Izin Penyimpanan",
|
||||
"setupStoragePermissionSubtitle": "Diperlukan untuk menyimpan file unduhan",
|
||||
"setupStoragePermissionGranted": "Izin diberikan",
|
||||
"setupStoragePermissionDenied": "Izin ditolak",
|
||||
"setupGrantPermission": "Berikan Izin",
|
||||
"setupDownloadLocation": "Lokasi Unduhan",
|
||||
"setupChooseFolder": "Pilih Folder",
|
||||
"setupContinue": "Lanjutkan",
|
||||
"setupSkip": "Lewati untuk sekarang",
|
||||
|
||||
"dialogCancel": "Batal",
|
||||
"dialogOk": "OK",
|
||||
"dialogSave": "Simpan",
|
||||
"dialogDelete": "Hapus",
|
||||
"dialogRetry": "Coba Lagi",
|
||||
"dialogClose": "Tutup",
|
||||
"dialogYes": "Ya",
|
||||
"dialogNo": "Tidak",
|
||||
"dialogClear": "Hapus",
|
||||
"dialogConfirm": "Konfirmasi",
|
||||
"dialogDone": "Selesai",
|
||||
|
||||
"dialogClearHistoryTitle": "Hapus Riwayat",
|
||||
"dialogClearHistoryMessage": "Apakah Anda yakin ingin menghapus semua riwayat unduhan? Ini tidak dapat dibatalkan.",
|
||||
"dialogDeleteSelectedTitle": "Hapus yang Dipilih",
|
||||
"dialogDeleteSelectedMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari riwayat?\n\nIni juga akan menghapus file dari penyimpanan.",
|
||||
"dialogImportPlaylistTitle": "Impor Playlist",
|
||||
"dialogImportPlaylistMessage": "Ditemukan {count} lagu di CSV. Tambahkan ke antrian unduhan?",
|
||||
|
||||
"snackbarAddedToQueue": "Menambahkan \"{trackName}\" ke antrian",
|
||||
"snackbarAddedTracksToQueue": "Menambahkan {count} lagu ke antrian",
|
||||
"snackbarAlreadyDownloaded": "\"{trackName}\" sudah diunduh",
|
||||
"snackbarHistoryCleared": "Riwayat dihapus",
|
||||
"snackbarCredentialsSaved": "Kredensial disimpan",
|
||||
"snackbarCredentialsCleared": "Kredensial dihapus",
|
||||
"snackbarDeletedTracks": "Menghapus {count} {count, plural, =1{lagu} other{lagu}}",
|
||||
"snackbarCannotOpenFile": "Tidak dapat membuka file: {error}",
|
||||
"snackbarFillAllFields": "Harap isi semua field",
|
||||
"snackbarViewQueue": "Lihat Antrian",
|
||||
|
||||
"errorRateLimited": "Dibatasi",
|
||||
"errorRateLimitedMessage": "Terlalu banyak permintaan. Harap tunggu sebentar sebelum mencari lagi.",
|
||||
"errorFailedToLoad": "Gagal memuat {item}",
|
||||
"errorNoTracksFound": "Tidak ada lagu ditemukan",
|
||||
"errorMissingExtensionSource": "Tidak dapat memuat {item}: sumber ekstensi tidak ada",
|
||||
|
||||
"statusQueued": "Mengantri",
|
||||
"statusDownloading": "Mengunduh",
|
||||
"statusFinalizing": "Menyelesaikan",
|
||||
"statusCompleted": "Selesai",
|
||||
"statusFailed": "Gagal",
|
||||
"statusSkipped": "Dilewati",
|
||||
"statusPaused": "Dijeda",
|
||||
|
||||
"actionPause": "Jeda",
|
||||
"actionResume": "Lanjutkan",
|
||||
"actionCancel": "Batal",
|
||||
"actionStop": "Hentikan",
|
||||
"actionSelect": "Pilih",
|
||||
"actionSelectAll": "Pilih Semua",
|
||||
"actionDeselect": "Batal Pilih",
|
||||
"actionPaste": "Tempel",
|
||||
"actionImportCsv": "Impor CSV",
|
||||
"actionRemoveCredentials": "Hapus Kredensial",
|
||||
"actionSaveCredentials": "Simpan Kredensial",
|
||||
|
||||
"selectionSelected": "{count} dipilih",
|
||||
"selectionAllSelected": "Semua lagu dipilih",
|
||||
"selectionTapToSelect": "Ketuk lagu untuk memilih",
|
||||
"selectionDeleteTracks": "Hapus {count} {count, plural, =1{lagu} other{lagu}}",
|
||||
"selectionSelectToDelete": "Pilih lagu untuk dihapus",
|
||||
|
||||
"progressFetchingMetadata": "Mengambil metadata... {current}/{total}",
|
||||
"progressReadingCsv": "Membaca CSV...",
|
||||
|
||||
"searchSongs": "Lagu",
|
||||
"searchArtists": "Artis",
|
||||
"searchAlbums": "Album",
|
||||
"searchPlaylists": "Playlist",
|
||||
|
||||
"tooltipPlay": "Putar",
|
||||
"tooltipCancel": "Batal",
|
||||
"tooltipStop": "Hentikan",
|
||||
"tooltipRetry": "Coba Lagi",
|
||||
"tooltipRemove": "Hapus",
|
||||
"tooltipClear": "Hapus",
|
||||
"tooltipPaste": "Tempel",
|
||||
|
||||
"filenameFormat": "Format Nama File",
|
||||
"filenameFormatPreview": "Pratinjau: {preview}",
|
||||
"folderOrganization": "Organisasi Folder",
|
||||
"folderOrganizationNone": "Tanpa organisasi",
|
||||
"folderOrganizationByArtist": "Berdasarkan Artis",
|
||||
"folderOrganizationByAlbum": "Berdasarkan Album",
|
||||
"folderOrganizationByArtistAlbum": "Artis/Album",
|
||||
|
||||
"updateAvailable": "Pembaruan Tersedia",
|
||||
"updateNewVersion": "Versi {version} tersedia",
|
||||
"updateDownload": "Unduh",
|
||||
"updateLater": "Nanti",
|
||||
"updateChangelog": "Log Perubahan",
|
||||
|
||||
"providerPriority": "Prioritas Provider",
|
||||
"providerPrioritySubtitle": "Seret untuk mengatur ulang provider unduhan",
|
||||
"metadataProviderPriority": "Prioritas Provider Metadata",
|
||||
"metadataProviderPrioritySubtitle": "Urutan yang digunakan saat mengambil metadata lagu",
|
||||
|
||||
"logTitle": "Log",
|
||||
"logCopy": "Salin Log",
|
||||
"logClear": "Hapus Log",
|
||||
"logShare": "Bagikan Log",
|
||||
"logEmpty": "Belum ada log",
|
||||
"logCopied": "Log disalin ke clipboard",
|
||||
|
||||
"credentialsTitle": "Kredensial Spotify",
|
||||
"credentialsDescription": "Masukkan Client ID dan Secret Anda untuk menggunakan kuota aplikasi Spotify Anda sendiri.",
|
||||
"credentialsClientId": "Client ID",
|
||||
"credentialsClientIdHint": "Tempel Client ID",
|
||||
"credentialsClientSecret": "Client Secret",
|
||||
"credentialsClientSecretHint": "Tempel Client Secret",
|
||||
|
||||
"channelStable": "Stabil",
|
||||
"channelPreview": "Preview",
|
||||
|
||||
"sectionSearchSource": "Sumber Pencarian",
|
||||
"sectionDownload": "Unduhan",
|
||||
"sectionPerformance": "Performa",
|
||||
"sectionApp": "Aplikasi",
|
||||
"sectionData": "Data",
|
||||
"sectionDebug": "Debug",
|
||||
"sectionService": "Layanan",
|
||||
"sectionAudioQuality": "Kualitas Audio",
|
||||
"sectionFileSettings": "Pengaturan File",
|
||||
"sectionColor": "Warna",
|
||||
"sectionTheme": "Tema",
|
||||
"sectionLayout": "Tata Letak",
|
||||
|
||||
"settingsAppearanceSubtitle": "Tema, warna, tampilan",
|
||||
"settingsDownloadSubtitle": "Layanan, kualitas, format nama file",
|
||||
"settingsOptionsSubtitle": "Fallback, lirik, cover art, pembaruan",
|
||||
"settingsExtensionsSubtitle": "Kelola provider unduhan",
|
||||
"settingsLogsSubtitle": "Lihat log aplikasi untuk debugging",
|
||||
|
||||
"loadingSharedLink": "Memuat link yang dibagikan...",
|
||||
"pressBackAgainToExit": "Tekan kembali sekali lagi untuk keluar",
|
||||
|
||||
"artistReleases": "{count, plural, =1{1 rilis} other{{count} rilis}}",
|
||||
"artistCompilations": "Kompilasi",
|
||||
|
||||
"tracksHeader": "Lagu",
|
||||
"downloadAllCount": "Unduh Semua ({count})",
|
||||
"tracksCount": "{count, plural, =1{1 lagu} other{{count} lagu}}",
|
||||
|
||||
"setupStorageAccessRequired": "Akses Penyimpanan Diperlukan",
|
||||
"setupStorageAccessMessage": "SpotiFLAC membutuhkan izin \"Akses semua file\" untuk menyimpan file musik ke folder pilihan Anda.",
|
||||
"setupStorageAccessMessageAndroid11": "Android 11+ memerlukan izin \"Akses semua file\" untuk menyimpan file ke folder unduhan pilihan Anda.",
|
||||
"setupOpenSettings": "Buka Pengaturan",
|
||||
"setupPermissionDeniedMessage": "Izin ditolak. Harap berikan semua izin untuk melanjutkan.",
|
||||
"setupPermissionRequired": "Izin {permissionType} Diperlukan",
|
||||
"setupPermissionRequiredMessage": "Izin {permissionType} diperlukan untuk pengalaman terbaik. Anda dapat mengubahnya nanti di Pengaturan.",
|
||||
"setupSelectDownloadFolder": "Pilih Folder Unduhan",
|
||||
"setupUseDefaultFolder": "Gunakan Folder Default?",
|
||||
"setupNoFolderSelected": "Tidak ada folder dipilih. Apakah Anda ingin menggunakan folder Musik default?",
|
||||
"setupUseDefault": "Gunakan Default",
|
||||
"setupDownloadLocationTitle": "Lokasi Unduhan",
|
||||
"setupDownloadLocationIosMessage": "Di iOS, unduhan disimpan ke folder Documents aplikasi. Anda dapat mengaksesnya melalui aplikasi Files.",
|
||||
"setupAppDocumentsFolder": "Folder Documents Aplikasi",
|
||||
"setupAppDocumentsFolderSubtitle": "Direkomendasikan - dapat diakses via aplikasi Files",
|
||||
"setupChooseFromFiles": "Pilih dari Files",
|
||||
"setupChooseFromFilesSubtitle": "Pilih lokasi iCloud atau lainnya",
|
||||
"setupIosEmptyFolderWarning": "Batasan iOS: Folder kosong tidak dapat dipilih. Pilih folder dengan minimal satu file.",
|
||||
"setupDownloadInFlac": "Unduh lagu Spotify dalam format FLAC",
|
||||
"setupStepStorage": "Penyimpanan",
|
||||
"setupStepNotification": "Notifikasi",
|
||||
"setupStepFolder": "Folder",
|
||||
"setupStepSpotify": "Spotify",
|
||||
"setupStepPermission": "Izin",
|
||||
"setupStorageGranted": "Izin Penyimpanan Diberikan!",
|
||||
"setupStorageRequired": "Izin Penyimpanan Diperlukan",
|
||||
"setupStorageDescription": "SpotiFLAC membutuhkan izin penyimpanan untuk menyimpan file musik yang diunduh.",
|
||||
"setupNotificationGranted": "Izin Notifikasi Diberikan!",
|
||||
"setupNotificationEnable": "Aktifkan Notifikasi",
|
||||
"setupNotificationDescription": "Dapatkan pemberitahuan saat unduhan selesai atau membutuhkan perhatian.",
|
||||
"setupFolderSelected": "Folder Unduhan Dipilih!",
|
||||
"setupFolderChoose": "Pilih Folder Unduhan",
|
||||
"setupFolderDescription": "Pilih folder tempat musik yang diunduh akan disimpan.",
|
||||
"setupChangeFolder": "Ubah Folder",
|
||||
"setupSelectFolder": "Pilih Folder",
|
||||
"setupSpotifyApiOptional": "Spotify API (Opsional)",
|
||||
"setupSpotifyApiDescription": "Tambahkan kredensial Spotify API untuk hasil pencarian lebih baik dan akses ke konten eksklusif Spotify.",
|
||||
"setupUseSpotifyApi": "Gunakan Spotify API",
|
||||
"setupEnterCredentialsBelow": "Masukkan kredensial Anda di bawah",
|
||||
"setupUsingDeezer": "Menggunakan Deezer (tidak perlu akun)",
|
||||
"setupEnterClientId": "Masukkan Spotify Client ID",
|
||||
"setupEnterClientSecret": "Masukkan Spotify Client Secret",
|
||||
"setupGetFreeCredentials": "Dapatkan kredensial API gratis dari Spotify Developer Dashboard.",
|
||||
"setupEnableNotifications": "Aktifkan Notifikasi",
|
||||
|
||||
"dialogImport": "Impor",
|
||||
"dialogDiscard": "Buang",
|
||||
"dialogRemove": "Hapus",
|
||||
"dialogUninstall": "Copot",
|
||||
"dialogDiscardChanges": "Buang Perubahan?",
|
||||
"dialogUnsavedChanges": "Anda memiliki perubahan yang belum disimpan. Apakah Anda ingin membuangnya?",
|
||||
"dialogDownloadFailed": "Unduhan Gagal",
|
||||
"dialogTrackLabel": "Lagu:",
|
||||
"dialogArtistLabel": "Artis:",
|
||||
"dialogErrorLabel": "Error:",
|
||||
"dialogClearAll": "Hapus Semua",
|
||||
"dialogClearAllDownloads": "Apakah Anda yakin ingin menghapus semua unduhan?",
|
||||
"dialogRemoveFromDevice": "Hapus dari perangkat?",
|
||||
"dialogRemoveExtension": "Hapus Ekstensi",
|
||||
"dialogRemoveExtensionMessage": "Apakah Anda yakin ingin menghapus ekstensi ini? Tindakan ini tidak dapat dibatalkan.",
|
||||
"dialogUninstallExtension": "Copot Ekstensi?",
|
||||
"dialogUninstallExtensionMessage": "Apakah Anda yakin ingin menghapus {extensionName}?",
|
||||
|
||||
"snackbarFailedToLoad": "Gagal memuat: {error}",
|
||||
"snackbarUrlCopied": "URL {platform} disalin ke clipboard",
|
||||
"snackbarFileNotFound": "File tidak ditemukan",
|
||||
"snackbarSelectExtFile": "Harap pilih file .spotiflac-ext",
|
||||
"snackbarProviderPrioritySaved": "Prioritas provider disimpan",
|
||||
"snackbarMetadataProviderSaved": "Prioritas provider metadata disimpan",
|
||||
"snackbarExtensionInstalled": "{extensionName} terpasang.",
|
||||
"snackbarExtensionUpdated": "{extensionName} diperbarui.",
|
||||
"snackbarFailedToInstall": "Gagal memasang ekstensi",
|
||||
"snackbarFailedToUpdate": "Gagal memperbarui ekstensi",
|
||||
|
||||
"storeFilterAll": "Semua",
|
||||
"storeFilterMetadata": "Metadata",
|
||||
"storeFilterDownload": "Unduhan",
|
||||
"storeFilterUtility": "Utilitas",
|
||||
"storeFilterLyrics": "Lirik",
|
||||
"storeFilterIntegration": "Integrasi",
|
||||
"storeClearFilters": "Hapus filter",
|
||||
"storeNoResults": "Tidak ada ekstensi ditemukan",
|
||||
|
||||
"extensionProviderPriority": "Prioritas Provider",
|
||||
"extensionInstallButton": "Pasang Ekstensi",
|
||||
"extensionDefaultProvider": "Default (Deezer/Spotify)",
|
||||
"extensionDefaultProviderSubtitle": "Gunakan pencarian bawaan",
|
||||
"extensionAuthor": "Pembuat",
|
||||
"extensionId": "ID",
|
||||
"extensionError": "Error",
|
||||
"extensionCapabilities": "Kemampuan",
|
||||
"extensionMetadataProvider": "Provider Metadata",
|
||||
"extensionDownloadProvider": "Provider Unduhan",
|
||||
"extensionLyricsProvider": "Provider Lirik",
|
||||
"extensionUrlHandler": "Penanganan URL",
|
||||
"extensionQualityOptions": "Opsi Kualitas",
|
||||
"extensionPostProcessingHooks": "Hook Pasca-Pemrosesan",
|
||||
"extensionPermissions": "Izin",
|
||||
"extensionSettings": "Pengaturan",
|
||||
"extensionRemoveButton": "Hapus Ekstensi",
|
||||
"extensionUpdated": "Diperbarui",
|
||||
"extensionMinAppVersion": "Versi App Minimum",
|
||||
|
||||
"qualityFlacLossless": "FLAC Lossless",
|
||||
"qualityFlacLosslessSubtitle": "16-bit / 44.1kHz",
|
||||
"qualityHiResFlac": "Hi-Res FLAC",
|
||||
"qualityHiResFlacSubtitle": "24-bit / hingga 96kHz",
|
||||
"qualityHiResFlacMax": "Hi-Res FLAC Max",
|
||||
"qualityHiResFlacMaxSubtitle": "24-bit / hingga 192kHz",
|
||||
"qualityNote": "Kualitas sebenarnya tergantung ketersediaan lagu dari layanan",
|
||||
|
||||
"downloadAskBeforeDownload": "Tanya Sebelum Unduh",
|
||||
"downloadDirectory": "Direktori Unduhan",
|
||||
"downloadSeparateSinglesFolder": "Folder Singles Terpisah",
|
||||
"downloadAlbumFolderStructure": "Struktur Folder Album",
|
||||
"downloadSaveFormat": "Simpan Format",
|
||||
"downloadSelectService": "Pilih Layanan",
|
||||
"downloadSelectQuality": "Pilih Kualitas",
|
||||
"downloadFrom": "Unduh Dari",
|
||||
"downloadDefaultQualityLabel": "Kualitas Default",
|
||||
"downloadBestAvailable": "Terbaik tersedia",
|
||||
|
||||
"folderNone": "Tidak ada",
|
||||
"folderNoneSubtitle": "Simpan semua file langsung ke folder unduhan",
|
||||
"folderArtist": "Artis",
|
||||
"folderArtistSubtitle": "Nama Artis/namafile",
|
||||
"folderAlbum": "Album",
|
||||
"folderAlbumSubtitle": "Nama Album/namafile",
|
||||
"folderArtistAlbum": "Artis/Album",
|
||||
"folderArtistAlbumSubtitle": "Nama Artis/Nama Album/namafile",
|
||||
|
||||
"serviceTidal": "Tidal",
|
||||
"serviceQobuz": "Qobuz",
|
||||
"serviceAmazon": "Amazon",
|
||||
"serviceDeezer": "Deezer",
|
||||
"serviceSpotify": "Spotify",
|
||||
|
||||
"logSearchHint": "Cari log...",
|
||||
"logFilterLevel": "Level",
|
||||
"logFilterSection": "Filter",
|
||||
"logShareLogs": "Bagikan log",
|
||||
"logClearLogs": "Hapus log",
|
||||
"logClearLogsTitle": "Hapus Log",
|
||||
"logClearLogsMessage": "Apakah Anda yakin ingin menghapus semua log?",
|
||||
"logIspBlocking": "PEMBLOKIRAN ISP TERDETEKSI",
|
||||
"logRateLimited": "DIBATASI",
|
||||
"logNetworkError": "ERROR JARINGAN",
|
||||
"logTrackNotFound": "LAGU TIDAK DITEMUKAN",
|
||||
|
||||
"appearanceAmoledDark": "AMOLED Gelap",
|
||||
"appearanceAmoledDarkSubtitle": "Latar belakang hitam murni",
|
||||
"appearanceChooseAccentColor": "Pilih Warna Aksen",
|
||||
"appearanceChooseTheme": "Mode Tema",
|
||||
|
||||
"updateStartingDownload": "Memulai unduhan...",
|
||||
"updateDownloadFailed": "Unduhan gagal",
|
||||
"updateFailedMessage": "Gagal mengunduh pembaruan",
|
||||
"updateNewVersionReady": "Versi baru sudah siap",
|
||||
"updateCurrent": "Saat ini",
|
||||
"updateNew": "Baru",
|
||||
"updateDownloading": "Mengunduh...",
|
||||
"updateWhatsNew": "Yang Baru",
|
||||
"updateDownloadInstall": "Unduh & Pasang",
|
||||
"updateDontRemind": "Jangan ingatkan",
|
||||
|
||||
"trackCopyFilePath": "Salin lokasi file",
|
||||
"trackRemoveFromDevice": "Hapus dari perangkat",
|
||||
"trackLoadLyrics": "Muat Lirik",
|
||||
|
||||
"dateToday": "Hari ini",
|
||||
"dateYesterday": "Kemarin",
|
||||
"dateDaysAgo": "{count} hari lalu",
|
||||
"dateWeeksAgo": "{count} minggu lalu",
|
||||
"dateMonthsAgo": "{count} bulan lalu",
|
||||
|
||||
"concurrentSequential": "Berurutan",
|
||||
"concurrentParallel2": "2 Paralel",
|
||||
"concurrentParallel3": "3 Paralel",
|
||||
|
||||
"filenameAvailablePlaceholders": "Placeholder yang tersedia:",
|
||||
"filenameHint": "{artist} - {title}",
|
||||
|
||||
"tapToSeeError": "Ketuk untuk melihat detail error",
|
||||
|
||||
"setupProceedToNextStep": "Anda dapat melanjutkan ke langkah berikutnya.",
|
||||
"setupNotificationProgressDescription": "Anda akan menerima notifikasi progres unduhan.",
|
||||
"setupNotificationBackgroundDescription": "Dapatkan notifikasi tentang progres dan penyelesaian unduhan. Ini membantu Anda melacak unduhan saat aplikasi di latar belakang.",
|
||||
"setupSkipForNow": "Lewati untuk sekarang",
|
||||
"setupBack": "Kembali",
|
||||
"setupNext": "Lanjut",
|
||||
"setupGetStarted": "Mulai",
|
||||
"setupSkipAndStart": "Lewati & Mulai",
|
||||
"setupAllowAccessToManageFiles": "Harap aktifkan \"Izinkan akses untuk mengelola semua file\" di layar berikutnya.",
|
||||
"setupGetCredentialsFromSpotify": "Dapatkan kredensial dari developer.spotify.com",
|
||||
|
||||
"trackMetadata": "Metadata",
|
||||
"trackFileInfo": "Info File",
|
||||
"trackLyrics": "Lirik",
|
||||
"trackFileNotFound": "File tidak ditemukan",
|
||||
"trackOpenInDeezer": "Buka di Deezer",
|
||||
"trackOpenInSpotify": "Buka di Spotify",
|
||||
"trackTrackName": "Nama lagu",
|
||||
"trackArtist": "Artis",
|
||||
"trackAlbumArtist": "Artis album",
|
||||
"trackAlbum": "Album",
|
||||
"trackTrackNumber": "Nomor lagu",
|
||||
"trackDiscNumber": "Nomor disc",
|
||||
"trackDuration": "Durasi",
|
||||
"trackAudioQuality": "Kualitas audio",
|
||||
"trackReleaseDate": "Tanggal rilis",
|
||||
"trackDownloaded": "Diunduh",
|
||||
"trackCopyLyrics": "Salin lirik",
|
||||
"trackLyricsNotAvailable": "Lirik tidak tersedia untuk lagu ini",
|
||||
"trackLyricsTimeout": "Permintaan timeout. Coba lagi nanti.",
|
||||
"trackLyricsLoadFailed": "Gagal memuat lirik",
|
||||
"trackCopiedToClipboard": "Disalin ke clipboard",
|
||||
"trackDeleteConfirmTitle": "Hapus dari perangkat?",
|
||||
"trackDeleteConfirmMessage": "Ini akan menghapus file unduhan secara permanen dan menghapusnya dari riwayat Anda.",
|
||||
"trackCannotOpen": "Tidak dapat membuka: {message}",
|
||||
|
||||
"logFilterBySeverity": "Filter log berdasarkan tingkat keparahan",
|
||||
"logNoLogsYet": "Belum ada log",
|
||||
"logNoLogsYetSubtitle": "Log akan muncul di sini saat Anda menggunakan aplikasi",
|
||||
"logIssueSummary": "Ringkasan Masalah",
|
||||
"logIspBlockingDescription": "ISP Anda mungkin memblokir akses ke layanan unduhan",
|
||||
"logIspBlockingSuggestion": "Coba gunakan VPN atau ubah DNS ke 1.1.1.1 atau 8.8.8.8",
|
||||
"logRateLimitedDescription": "Terlalu banyak permintaan ke layanan",
|
||||
"logRateLimitedSuggestion": "Tunggu beberapa menit sebelum mencoba lagi",
|
||||
"logNetworkErrorDescription": "Masalah koneksi terdeteksi",
|
||||
"logNetworkErrorSuggestion": "Periksa koneksi internet Anda",
|
||||
"logTrackNotFoundDescription": "Beberapa lagu tidak dapat ditemukan di layanan unduhan",
|
||||
"logTrackNotFoundSuggestion": "Lagu mungkin tidak tersedia dalam kualitas lossless",
|
||||
"logTotalErrors": "Total error: {count}",
|
||||
"logAffected": "Terpengaruh: {domains}",
|
||||
"logEntriesFiltered": "Entri ({count} difilter)",
|
||||
"logEntries": "Entri ({count})",
|
||||
|
||||
"extensionsProviderPrioritySection": "Prioritas Provider",
|
||||
"extensionsInstalledSection": "Ekstensi Terpasang",
|
||||
"extensionsNoExtensions": "Tidak ada ekstensi terpasang",
|
||||
"extensionsNoExtensionsSubtitle": "Pasang file .spotiflac-ext untuk menambahkan provider baru",
|
||||
"extensionsInstallButton": "Pasang Ekstensi",
|
||||
"extensionsInfoTip": "Ekstensi dapat menambahkan provider metadata dan unduhan baru. Hanya pasang ekstensi dari sumber terpercaya.",
|
||||
"extensionsInstalledSuccess": "Ekstensi berhasil dipasang",
|
||||
"extensionsDownloadPriority": "Prioritas Unduhan",
|
||||
"extensionsDownloadPrioritySubtitle": "Atur urutan layanan unduhan",
|
||||
"extensionsNoDownloadProvider": "Tidak ada ekstensi dengan provider unduhan",
|
||||
"extensionsMetadataPriority": "Prioritas Metadata",
|
||||
"extensionsMetadataPrioritySubtitle": "Atur urutan sumber pencarian & metadata",
|
||||
"extensionsNoMetadataProvider": "Tidak ada ekstensi dengan provider metadata",
|
||||
"extensionsSearchProvider": "Provider Pencarian",
|
||||
"extensionsNoCustomSearch": "Tidak ada ekstensi dengan pencarian kustom",
|
||||
"extensionsSearchProviderDescription": "Pilih layanan yang digunakan untuk mencari lagu",
|
||||
"extensionsCustomSearch": "Pencarian kustom",
|
||||
"extensionsErrorLoading": "Error memuat ekstensi",
|
||||
|
||||
"extensionCustomTrackMatching": "Pencocokan Lagu Kustom",
|
||||
"extensionPostProcessing": "Pasca-Pemrosesan",
|
||||
"extensionHooksAvailable": "{count} hook tersedia",
|
||||
"extensionPatternsCount": "{count} pola",
|
||||
"extensionStrategy": "Strategi: {strategy}",
|
||||
|
||||
"aboutDoubleDouble": "DoubleDouble",
|
||||
"aboutDoubleDoubleDesc": "API luar biasa untuk unduhan Amazon Music. Terima kasih sudah membuatnya gratis!",
|
||||
"aboutDabMusic": "DAB Music",
|
||||
"aboutDabMusicDesc": "API streaming Qobuz terbaik. Unduhan Hi-Res tidak akan mungkin tanpa ini!",
|
||||
|
||||
"queueTitle": "Antrian Unduhan",
|
||||
"queueClearAll": "Hapus Semua",
|
||||
"queueClearAllMessage": "Apakah Anda yakin ingin menghapus semua unduhan?",
|
||||
|
||||
"albumFolderArtistAlbum": "Artis / Album",
|
||||
"albumFolderArtistAlbumSubtitle": "Albums/Nama Artis/Nama Album/",
|
||||
"albumFolderArtistYearAlbum": "Artis / [Tahun] Album",
|
||||
"albumFolderArtistYearAlbumSubtitle": "Albums/Nama Artis/[2005] Nama Album/",
|
||||
"albumFolderAlbumOnly": "Album Saja",
|
||||
"albumFolderAlbumOnlySubtitle": "Albums/Nama Album/",
|
||||
"albumFolderYearAlbum": "[Tahun] Album",
|
||||
"albumFolderYearAlbumSubtitle": "Albums/[2005] Nama Album/",
|
||||
|
||||
"downloadedAlbumDeleteSelected": "Hapus yang Dipilih",
|
||||
"downloadedAlbumDeleteMessage": "Hapus {count} {count, plural, =1{lagu} other{lagu}} dari album ini?\n\nIni juga akan menghapus file dari penyimpanan.",
|
||||
|
||||
"utilityFunctions": "Fungsi Utilitas",
|
||||
|
||||
"aboutMobileDeveloper": "Pengembang versi mobile",
|
||||
"aboutOriginalCreator": "Pembuat SpotiFLAC asli",
|
||||
"aboutLogoArtist": "Seniman berbakat yang membuat logo aplikasi kita yang indah!",
|
||||
"aboutBinimumDesc": "Pembuat QQDL & HiFi API. Tanpa API ini, unduhan Tidal tidak akan ada!",
|
||||
"aboutSachinsenalDesc": "Pembuat proyek HiFi asli. Fondasi dari integrasi Tidal!",
|
||||
"aboutMobileSource": "Kode sumber mobile",
|
||||
"aboutPCSource": "Kode sumber PC",
|
||||
"aboutReportIssue": "Laporkan masalah",
|
||||
"aboutReportIssueSubtitle": "Laporkan masalah yang Anda temui",
|
||||
"aboutFeatureRequest": "Permintaan fitur",
|
||||
"aboutFeatureRequestSubtitle": "Sarankan fitur baru untuk aplikasi",
|
||||
"aboutBuyMeCoffee": "Belikan saya kopi",
|
||||
"aboutBuyMeCoffeeSubtitle": "Dukung pengembangan di Ko-fi",
|
||||
"aboutVersion": "Versi",
|
||||
"aboutAppDescription": "Unduh lagu Spotify dalam kualitas lossless dari Tidal, Qobuz, dan Amazon Music.",
|
||||
|
||||
"providerPriorityTitle": "Prioritas Provider",
|
||||
"providerPriorityDescription": "Seret untuk mengatur ulang urutan provider unduhan. Aplikasi akan mencoba provider dari atas ke bawah saat mengunduh lagu.",
|
||||
"providerPriorityInfo": "Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.",
|
||||
"providerBuiltIn": "Bawaan",
|
||||
"providerExtension": "Ekstensi",
|
||||
|
||||
"metadataProviderPriorityTitle": "Prioritas Metadata",
|
||||
"metadataProviderPriorityDescription": "Seret untuk mengatur ulang urutan provider metadata. Aplikasi akan mencoba provider dari atas ke bawah saat mencari lagu dan mengambil metadata.",
|
||||
"metadataProviderPriorityInfo": "Deezer tidak memiliki batas rate dan direkomendasikan sebagai utama. Spotify mungkin membatasi rate setelah banyak permintaan.",
|
||||
"metadataNoRateLimits": "Tidak ada batas rate",
|
||||
"metadataMayRateLimit": "Mungkin dibatasi rate",
|
||||
|
||||
"queueEmpty": "Tidak ada unduhan dalam antrian",
|
||||
"queueEmptySubtitle": "Tambahkan lagu dari layar beranda",
|
||||
"queueClearCompleted": "Hapus yang selesai",
|
||||
"queueDownloadFailed": "Unduhan Gagal",
|
||||
"queueTrackLabel": "Lagu:",
|
||||
"queueArtistLabel": "Artis:",
|
||||
"queueErrorLabel": "Error:",
|
||||
"queueUnknownError": "Error tidak diketahui",
|
||||
|
||||
"downloadedAlbumTracksHeader": "Lagu",
|
||||
"downloadedAlbumDownloadedCount": "{count} diunduh",
|
||||
"downloadedAlbumSelectedCount": "{count} dipilih",
|
||||
"downloadedAlbumAllSelected": "Semua lagu dipilih",
|
||||
"downloadedAlbumTapToSelect": "Ketuk lagu untuk memilih",
|
||||
"downloadedAlbumDeleteCount": "Hapus {count} {count, plural, =1{lagu} other{lagu}}",
|
||||
"downloadedAlbumSelectToDelete": "Pilih lagu untuk dihapus",
|
||||
|
||||
"folderOrganizationDescription": "Atur file yang diunduh ke dalam folder",
|
||||
"folderOrganizationNone": "Tidak ada",
|
||||
"folderOrganizationNoneSubtitle": "Semua file di folder unduhan",
|
||||
"folderOrganizationByArtist": "Berdasarkan Artis",
|
||||
"folderOrganizationByArtistSubtitle": "Folder terpisah untuk setiap artis",
|
||||
"folderOrganizationByAlbum": "Berdasarkan Album",
|
||||
"folderOrganizationByAlbumSubtitle": "Folder terpisah untuk setiap album",
|
||||
"folderOrganizationByArtistAlbum": "Berdasarkan Artis & Album",
|
||||
"folderOrganizationByArtistAlbumSubtitle": "Folder bersarang untuk artis dan album"
|
||||
}
|
||||
11
lib/l10n/l10n.dart
Normal file
11
lib/l10n/l10n.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
|
||||
export 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
|
||||
/// Extension to easily access AppLocalizations from BuildContext
|
||||
extension AppLocalizationsX on BuildContext {
|
||||
/// Get the AppLocalizations instance
|
||||
/// Usage: context.l10n.navHome
|
||||
AppLocalizations get l10n => AppLocalizations.of(this);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/models/download_item.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
|
|
@ -260,7 +261,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
|||
children: [
|
||||
Icon(Icons.music_note, size: 14, color: colorScheme.onSecondaryContainer),
|
||||
const SizedBox(width: 4),
|
||||
Text('${tracks.length} tracks', style: TextStyle(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
Text(context.l10n.tracksCount(tracks.length), style: TextStyle(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -269,7 +270,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
|||
FilledButton.icon(
|
||||
onPressed: () => _downloadAll(context),
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text('Download All (${tracks.length})'),
|
||||
label: Text(context.l10n.downloadAllCount(tracks.length)),
|
||||
style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))),
|
||||
),
|
||||
],
|
||||
|
|
@ -289,7 +290,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
|||
children: [
|
||||
Icon(Icons.queue_music, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
Text(context.l10n.tracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -324,12 +325,12 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
|||
coverUrl: track.coverUrl,
|
||||
onSelect: (quality, service) {
|
||||
ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -344,12 +345,12 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
|||
artistName: widget.albumName,
|
||||
onSelect: (quality, service) {
|
||||
ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, service, qualityOverride: quality);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length))));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, settings.defaultService);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +376,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Rate Limited',
|
||||
context.l10n.errorRateLimited,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -383,7 +384,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Too many requests. Please wait a moment and try again.',
|
||||
context.l10n.errorRateLimitedMessage,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontSize: 12,
|
||||
|
|
@ -476,7 +477,7 @@ class _AlbumTrackItem extends ConsumerWidget {
|
|||
final fileExists = await File(historyItem.filePath).exists();
|
||||
if (fileExists) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('"${track.name}" already downloaded')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/track_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
|
|
@ -147,9 +148,9 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
child: _buildErrorWidget(_error!, colorScheme),
|
||||
)),
|
||||
if (!_isLoadingDiscography && _error == null) ...[
|
||||
if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Albums', albumsOnly, colorScheme)),
|
||||
if (singles.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Singles & EPs', singles, colorScheme)),
|
||||
if (compilations.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection('Compilations', compilations, colorScheme)),
|
||||
if (albumsOnly.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistAlbums, albumsOnly, colorScheme)),
|
||||
if (singles.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistSingles, singles, colorScheme)),
|
||||
if (compilations.isNotEmpty) SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistCompilations, compilations, colorScheme)),
|
||||
],
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
||||
],
|
||||
|
|
@ -255,7 +256,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
children: [
|
||||
Icon(Icons.album, size: 14, color: colorScheme.onPrimaryContainer),
|
||||
const SizedBox(width: 4),
|
||||
Text('${_albums!.length} releases', style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
Text(context.l10n.artistReleases(_albums!.length), style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -327,7 +328,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
const Spacer(),
|
||||
Text(
|
||||
album.totalTracks > 0
|
||||
? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} • ${album.totalTracks} tracks'
|
||||
? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} • ${context.l10n.tracksCount(album.totalTracks)}'
|
||||
: album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 11),
|
||||
maxLines: 1,
|
||||
|
|
@ -394,7 +395,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Rate Limited',
|
||||
context.l10n.errorRateLimited,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -402,7 +403,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Too many requests. Please wait a moment and try again.',
|
||||
context.l10n.errorRateLimitedMessage,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontSize: 12,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/utils/mime_utils.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
|
|
@ -84,19 +85,19 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete Selected'),
|
||||
content: Text('Delete $count ${count == 1 ? 'track' : 'tracks'} from this album?\n\nThis will also delete the files from storage.'),
|
||||
title: Text(context.l10n.downloadedAlbumDeleteSelected),
|
||||
content: Text(context.l10n.downloadedAlbumDeleteMessage(count)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: const Text('Delete'),
|
||||
child: Text(context.l10n.dialogDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -125,7 +126,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Deleted $deletedCount ${deletedCount == 1 ? 'track' : 'tracks'}')),
|
||||
SnackBar(content: Text(context.l10n.snackbarDeletedTracks(deletedCount))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -138,7 +139,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Cannot open file: $e')),
|
||||
SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -323,7 +324,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
children: [
|
||||
Icon(Icons.download_done, size: 14, color: colorScheme.onPrimaryContainer),
|
||||
const SizedBox(width: 4),
|
||||
Text('${tracks.length} downloaded', style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
Text(context.l10n.downloadedAlbumDownloadedCount(tracks.length), style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -376,13 +377,13 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
children: [
|
||||
Icon(Icons.queue_music, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
Text(context.l10n.downloadedAlbumTracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
const Spacer(),
|
||||
if (!_isSelectionMode)
|
||||
TextButton.icon(
|
||||
onPressed: tracks.isNotEmpty ? () => _enterSelectionMode(tracks.first.id) : null,
|
||||
icon: const Icon(Icons.checklist, size: 18),
|
||||
label: const Text('Select'),
|
||||
label: Text(context.l10n.actionSelect),
|
||||
style: TextButton.styleFrom(visualDensity: VisualDensity.compact),
|
||||
),
|
||||
],
|
||||
|
|
@ -523,11 +524,11 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$selectedCount selected',
|
||||
context.l10n.downloadedAlbumSelectedCount(selectedCount),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
allSelected ? 'All tracks selected' : 'Tap tracks to select',
|
||||
allSelected ? context.l10n.downloadedAlbumAllSelected : context.l10n.downloadedAlbumTapToSelect,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
|
|
@ -542,7 +543,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
}
|
||||
},
|
||||
icon: Icon(allSelected ? Icons.deselect : Icons.select_all, size: 20),
|
||||
label: Text(allSelected ? 'Deselect' : 'Select All'),
|
||||
label: Text(allSelected ? context.l10n.actionDeselect : context.l10n.actionSelectAll),
|
||||
style: TextButton.styleFrom(foregroundColor: colorScheme.primary),
|
||||
),
|
||||
],
|
||||
|
|
@ -555,8 +556,8 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
|||
icon: const Icon(Icons.delete_outline),
|
||||
label: Text(
|
||||
selectedCount > 0
|
||||
? 'Delete $selectedCount ${selectedCount == 1 ? 'track' : 'tracks'}'
|
||||
: 'Select tracks to delete',
|
||||
? context.l10n.downloadedAlbumDeleteCount(selectedCount)
|
||||
: context.l10n.downloadedAlbumSelectToDelete,
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: selectedCount > 0 ? colorScheme.error : colorScheme.surfaceContainerHighest,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/track_provider.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
|
|
@ -202,12 +203,12 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
coverUrl: track.coverUrl,
|
||||
onSelect: (quality, service) {
|
||||
ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,8 +239,8 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
const SizedBox(height: 16),
|
||||
Text(
|
||||
totalTracks > 0
|
||||
? 'Fetching metadata... $currentProgress/$totalTracks'
|
||||
: 'Reading CSV...',
|
||||
? context.l10n.progressFetchingMetadata(currentProgress, totalTracks)
|
||||
: context.l10n.progressReadingCsv,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -274,16 +275,16 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
final confirmed = await showDialog<bool>(
|
||||
context: this.context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: const Text('Import Playlist'),
|
||||
content: Text('Found ${tracks.length} tracks in CSV. Add them to download queue?'),
|
||||
title: Text(context.l10n.dialogImportPlaylistTitle),
|
||||
content: Text(context.l10n.dialogImportPlaylistMessage(tracks.length)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogCtx, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogCtx, true),
|
||||
child: const Text('Import'),
|
||||
child: Text(context.l10n.dialogImport),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -294,9 +295,9 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Added ${tracks.length} tracks to queue'),
|
||||
content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length)),
|
||||
action: SnackBarAction(
|
||||
label: 'View Queue',
|
||||
label: context.l10n.snackbarViewQueue,
|
||||
onPressed: () {
|
||||
// Navigate to queue tab (handled by main_shell index)
|
||||
// We don't have direct access to set index here easily without provider
|
||||
|
|
@ -364,7 +365,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
title: Text(
|
||||
'Home',
|
||||
context.l10n.homeTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (14 * expandRatio), // 20 -> 34
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -418,7 +419,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Paste a Spotify link or search by name',
|
||||
context.l10n.homeSubtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
|
|
@ -450,7 +451,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'Supports: Track, Album, Playlist, Artist URLs',
|
||||
context.l10n.homeSupports,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
|
|
@ -490,7 +491,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(
|
||||
'Recent',
|
||||
context.l10n.homeRecent,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -663,7 +664,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
if (artistItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text('Artists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
)),
|
||||
if (artistItems.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
|
|
@ -698,7 +699,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
if (albumItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text('Albums', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
child: Text(context.l10n.searchAlbums, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
)),
|
||||
if (albumItems.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
|
|
@ -733,7 +734,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
if (playlistItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text('Playlists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
child: Text(context.l10n.searchPlaylists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
)),
|
||||
if (playlistItems.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
|
|
@ -768,7 +769,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
if (realTracks.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text('Songs', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
child: Text(context.l10n.searchSongs, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
)),
|
||||
|
||||
// Track list in grouped card
|
||||
|
|
@ -813,7 +814,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text('Artists', style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
child: Text(context.l10n.searchArtists, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
|
|
@ -901,7 +902,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
final extensionId = albumItem.source;
|
||||
if (extensionId == null || extensionId.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Cannot load album: missing extension source')),
|
||||
SnackBar(content: Text(context.l10n.errorMissingExtensionSource('album'))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -923,7 +924,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
final extensionId = playlistItem.source;
|
||||
if (extensionId == null || extensionId.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Cannot load playlist: missing extension source')),
|
||||
SnackBar(content: Text(context.l10n.errorMissingExtensionSource('playlist'))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -945,7 +946,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
|||
final extensionId = artistItem.source;
|
||||
if (extensionId == null || extensionId.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Cannot load artist: missing extension source')),
|
||||
SnackBar(content: Text(context.l10n.errorMissingExtensionSource('artist'))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1206,7 +1207,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
|||
// File exists, show snackbar
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('"${track.name}" already downloaded')),
|
||||
SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))),
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
@ -1511,7 +1512,7 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
|||
children: [
|
||||
Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: _fetchTracks, child: const Text('Retry')),
|
||||
ElevatedButton(onPressed: _fetchTracks, child: Text(context.l10n.dialogRetry)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -1649,7 +1650,7 @@ class _ExtensionPlaylistScreenState extends ConsumerState<ExtensionPlaylistScree
|
|||
children: [
|
||||
Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: _fetchTracks, child: const Text('Retry')),
|
||||
ElevatedButton(onPressed: _fetchTracks, child: Text(context.l10n.dialogRetry)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -1772,7 +1773,7 @@ class _ExtensionArtistScreenState extends ConsumerState<ExtensionArtistScreen> {
|
|||
children: [
|
||||
Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: _fetchArtist, child: const Text('Retry')),
|
||||
ElevatedButton(onPressed: _fetchArtist, child: Text(context.l10n.dialogRetry)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/providers/track_provider.dart';
|
||||
|
|
@ -77,7 +78,7 @@ class _MainShellState extends ConsumerState<MainShell> {
|
|||
// Show snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Loading shared link...')),
|
||||
SnackBar(content: Text(context.l10n.loadingSharedLink)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -162,9 +163,9 @@ class _MainShellState extends ConsumerState<MainShell> {
|
|||
} else {
|
||||
_lastBackPress = now;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Press back again to exit'),
|
||||
duration: Duration(seconds: 2),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.pressBackAgainToExit),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
|
@ -201,11 +202,12 @@ class _MainShellState extends ConsumerState<MainShell> {
|
|||
const SettingsTab(),
|
||||
];
|
||||
|
||||
final l10n = context.l10n;
|
||||
final destinations = <NavigationDestination>[
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
selectedIcon: const Icon(Icons.home),
|
||||
label: l10n.navHome,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Badge(
|
||||
|
|
@ -218,18 +220,18 @@ class _MainShellState extends ConsumerState<MainShell> {
|
|||
label: Text('$queueState'),
|
||||
child: const Icon(Icons.history),
|
||||
),
|
||||
label: 'History',
|
||||
label: l10n.navHistory,
|
||||
),
|
||||
if (showStore)
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.store_outlined),
|
||||
selectedIcon: Icon(Icons.store),
|
||||
label: 'Store',
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.store_outlined),
|
||||
selectedIcon: const Icon(Icons.store),
|
||||
label: l10n.navStore,
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
selectedIcon: const Icon(Icons.settings),
|
||||
label: l10n.navSettings,
|
||||
),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/models/download_item.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
|
|
@ -114,7 +115,7 @@ class PlaylistScreen extends ConsumerWidget {
|
|||
children: [
|
||||
Icon(Icons.playlist_play, size: 14, color: colorScheme.onTertiaryContainer),
|
||||
const SizedBox(width: 4),
|
||||
Text('${tracks.length} tracks', style: TextStyle(color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
Text(context.l10n.tracksCount(tracks.length), style: TextStyle(color: colorScheme.onTertiaryContainer, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -122,7 +123,7 @@ class PlaylistScreen extends ConsumerWidget {
|
|||
FilledButton.icon(
|
||||
onPressed: () => _downloadAll(context, ref),
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text('Download All (${tracks.length})'),
|
||||
label: Text(context.l10n.downloadAllCount(tracks.length)),
|
||||
style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(52), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))),
|
||||
),
|
||||
],
|
||||
|
|
@ -141,7 +142,7 @@ class PlaylistScreen extends ConsumerWidget {
|
|||
children: [
|
||||
Icon(Icons.queue_music, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('Tracks', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
Text(context.l10n.tracksHeader, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -176,12 +177,12 @@ class PlaylistScreen extends ConsumerWidget {
|
|||
coverUrl: track.coverUrl,
|
||||
onSelect: (quality, service) {
|
||||
ref.read(downloadQueueProvider.notifier).addToQueue(track, service, qualityOverride: quality);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ref.read(downloadQueueProvider.notifier).addToQueue(track, settings.defaultService);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,12 +196,12 @@ class PlaylistScreen extends ConsumerWidget {
|
|||
artistName: playlistName,
|
||||
onSelect: (quality, service) {
|
||||
ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, service, qualityOverride: quality);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length))));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ref.read(downloadQueueProvider.notifier).addMultipleToQueue(tracks, settings.defaultService);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added ${tracks.length} tracks to queue')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedTracksToQueue(tracks.length))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +265,7 @@ class _PlaylistTrackItem extends ConsumerWidget {
|
|||
final fileExists = await File(historyItem.filePath).exists();
|
||||
if (fileExists) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('"${track.name}" already downloaded')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/download_item.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
|
||||
|
|
@ -14,19 +15,19 @@ class QueueScreen extends ConsumerWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Download Queue'),
|
||||
title: Text(context.l10n.queueTitle),
|
||||
actions: [
|
||||
if (queueState.items.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_sweep),
|
||||
onPressed: () => ref.read(downloadQueueProvider.notifier).clearCompleted(),
|
||||
tooltip: 'Clear completed',
|
||||
tooltip: context.l10n.queueClearCompleted,
|
||||
),
|
||||
if (queueState.items.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear_all),
|
||||
onPressed: () => _showClearAllDialog(context, ref),
|
||||
tooltip: 'Clear all',
|
||||
tooltip: context.l10n.queueClearAll,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -51,14 +52,14 @@ class QueueScreen extends ConsumerWidget {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No downloads in queue',
|
||||
context.l10n.queueEmpty,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Add tracks from the home screen',
|
||||
context.l10n.queueEmptySubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
|
|
@ -177,7 +178,7 @@ class QueueScreen extends ConsumerWidget {
|
|||
children: [
|
||||
Icon(Icons.error, color: colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Download Failed'),
|
||||
Text(context.l10n.queueDownloadFailed),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
|
|
@ -185,10 +186,10 @@ class QueueScreen extends ConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Track: ${item.track.name}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('Artist: ${item.track.artistName}'),
|
||||
Text('${context.l10n.queueTrackLabel} ${item.track.name}', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('${context.l10n.queueArtistLabel} ${item.track.artistName}'),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Error:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(context.l10n.queueErrorLabel, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
|
@ -197,7 +198,7 @@ class QueueScreen extends ConsumerWidget {
|
|||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
item.error ?? 'Unknown error',
|
||||
item.error ?? context.l10n.queueUnknownError,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
|
|
@ -211,7 +212,7 @@ class QueueScreen extends ConsumerWidget {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
child: Text(context.l10n.dialogClose),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -223,19 +224,19 @@ class QueueScreen extends ConsumerWidget {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear All'),
|
||||
content: const Text('Are you sure you want to clear all downloads?'),
|
||||
title: Text(context.l10n.queueClearAll),
|
||||
content: Text(context.l10n.queueClearAllMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(downloadQueueProvider.notifier).clearAll();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('Clear', style: TextStyle(color: colorScheme.error)),
|
||||
child: Text(context.l10n.dialogClear, style: TextStyle(color: colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/utils/mime_utils.dart';
|
||||
import 'package:spotiflac_android/models/download_item.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
|
|
@ -139,21 +140,19 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete Selected'),
|
||||
content: Text(
|
||||
'Delete $count ${count == 1 ? 'track' : 'tracks'} from history?\n\nThis will also delete the files from storage.',
|
||||
),
|
||||
title: Text(context.l10n.dialogDeleteSelectedTitle),
|
||||
content: Text(context.l10n.dialogDeleteSelectedMessage(count)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: const Text('Delete'),
|
||||
child: Text(context.l10n.dialogDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -184,9 +183,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Deleted $deletedCount ${deletedCount == 1 ? 'track' : 'tracks'}',
|
||||
),
|
||||
content: Text(context.l10n.snackbarDeletedTracks(deletedCount)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -235,7 +232,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Cannot open file: $e')));
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -493,7 +490,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
title: Text(
|
||||
'History',
|
||||
context.l10n.historyTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (14 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -590,7 +587,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
child: Row(
|
||||
children: [
|
||||
_FilterChip(
|
||||
label: 'All',
|
||||
label: context.l10n.historyFilterAll,
|
||||
count: allHistoryItems.length,
|
||||
isSelected: historyFilterMode == 'all',
|
||||
onTap: () {
|
||||
|
|
@ -599,7 +596,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
label: 'Albums',
|
||||
label: context.l10n.historyFilterAlbums,
|
||||
count: albumCount,
|
||||
isSelected: historyFilterMode == 'albums',
|
||||
onTap: () {
|
||||
|
|
@ -608,7 +605,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_FilterChip(
|
||||
label: 'Singles',
|
||||
label: context.l10n.historyFilterSingles,
|
||||
count: singleCount,
|
||||
isSelected: historyFilterMode == 'singles',
|
||||
onTap: () {
|
||||
|
|
@ -784,7 +781,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
|||
? () => _enterSelectionMode(historyItems.first.id)
|
||||
: null,
|
||||
icon: const Icon(Icons.checklist, size: 18),
|
||||
label: const Text('Select'),
|
||||
label: Text(context.l10n.actionSelect),
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
class AboutPage extends StatelessWidget {
|
||||
|
|
@ -41,7 +42,7 @@ class AboutPage extends StatelessWidget {
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
||||
title: Text(
|
||||
'About',
|
||||
context.l10n.aboutTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio), // 20 -> 28
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -62,27 +63,27 @@ class AboutPage extends StatelessWidget {
|
|||
),
|
||||
|
||||
// Contributors section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Contributors'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.aboutContributors),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
_ContributorItem(
|
||||
name: AppInfo.mobileAuthor,
|
||||
description: 'Mobile version developer',
|
||||
description: context.l10n.aboutMobileDeveloper,
|
||||
githubUsername: AppInfo.mobileAuthor,
|
||||
showDivider: true,
|
||||
),
|
||||
_ContributorItem(
|
||||
name: AppInfo.originalAuthor,
|
||||
description: 'Creator of the original SpotiFLAC',
|
||||
description: context.l10n.aboutOriginalCreator,
|
||||
githubUsername: AppInfo.originalAuthor,
|
||||
showDivider: true,
|
||||
),
|
||||
_ContributorItem(
|
||||
name: 'Amonoman',
|
||||
description: 'The talented artist who created our beautiful app logo!',
|
||||
description: context.l10n.aboutLogoArtist,
|
||||
githubUsername: 'Amonoman',
|
||||
showDivider: false,
|
||||
),
|
||||
|
|
@ -91,35 +92,35 @@ class AboutPage extends StatelessWidget {
|
|||
),
|
||||
|
||||
// Special Thanks section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Special Thanks'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.aboutSpecialThanks),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
_ContributorItem(
|
||||
name: 'binimum',
|
||||
description: 'The creator of QQDL & HiFi API. Without this API, Tidal downloads wouldn\'t exist!',
|
||||
description: context.l10n.aboutBinimumDesc,
|
||||
githubUsername: 'binimum',
|
||||
showDivider: true,
|
||||
),
|
||||
_ContributorItem(
|
||||
name: 'sachinsenal0x64',
|
||||
description: 'The original HiFi project creator. The foundation of Tidal integration!',
|
||||
description: context.l10n.aboutSachinsenalDesc,
|
||||
githubUsername: 'sachinsenal0x64',
|
||||
showDivider: true,
|
||||
),
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.cloud_outlined,
|
||||
title: 'DoubleDouble',
|
||||
subtitle: 'Amazing API for Amazon Music downloads. Thank you for making it free!',
|
||||
title: context.l10n.aboutDoubleDouble,
|
||||
subtitle: context.l10n.aboutDoubleDoubleDesc,
|
||||
onTap: () => _launchUrl('https://doubledouble.top'),
|
||||
showDivider: true,
|
||||
),
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.music_note_outlined,
|
||||
title: 'DAB Music',
|
||||
subtitle: 'The best Qobuz streaming API. Hi-Res downloads wouldn\'t be possible without this!',
|
||||
title: context.l10n.aboutDabMusic,
|
||||
subtitle: context.l10n.aboutDabMusicDesc,
|
||||
onTap: () => _launchUrl('https://dabmusic.xyz'),
|
||||
showDivider: false,
|
||||
),
|
||||
|
|
@ -128,37 +129,37 @@ class AboutPage extends StatelessWidget {
|
|||
),
|
||||
|
||||
// Links section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Links'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.aboutLinks),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.phone_android,
|
||||
title: 'Mobile source code',
|
||||
title: context.l10n.aboutMobileSource,
|
||||
subtitle: 'github.com/${AppInfo.githubRepo}',
|
||||
onTap: () => _launchUrl(AppInfo.githubUrl),
|
||||
showDivider: true,
|
||||
),
|
||||
SettingsItem(
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.computer,
|
||||
title: 'PC source code',
|
||||
title: context.l10n.aboutPCSource,
|
||||
subtitle: 'github.com/${AppInfo.originalAuthor}/SpotiFLAC',
|
||||
onTap: () => _launchUrl(AppInfo.originalGithubUrl),
|
||||
showDivider: true,
|
||||
),
|
||||
SettingsItem(
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.bug_report_outlined,
|
||||
title: 'Report an issue',
|
||||
subtitle: 'Report any problems you encounter',
|
||||
title: context.l10n.aboutReportIssue,
|
||||
subtitle: context.l10n.aboutReportIssueSubtitle,
|
||||
onTap: () => _launchUrl('${AppInfo.githubUrl}/issues/new'),
|
||||
showDivider: true,
|
||||
),
|
||||
SettingsItem(
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.lightbulb_outline,
|
||||
title: 'Feature request',
|
||||
subtitle: 'Suggest new features for the app',
|
||||
title: context.l10n.aboutFeatureRequest,
|
||||
subtitle: context.l10n.aboutFeatureRequestSubtitle,
|
||||
onTap: () => _launchUrl('${AppInfo.githubUrl}/issues/new'),
|
||||
showDivider: false,
|
||||
),
|
||||
|
|
@ -167,16 +168,16 @@ class AboutPage extends StatelessWidget {
|
|||
),
|
||||
|
||||
// Support section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Support'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.aboutSupport),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.coffee_outlined,
|
||||
title: 'Buy me a coffee',
|
||||
subtitle: 'Support development on Ko-fi',
|
||||
title: context.l10n.aboutBuyMeCoffee,
|
||||
subtitle: context.l10n.aboutBuyMeCoffeeSubtitle,
|
||||
onTap: () => _launchUrl(AppInfo.kofiUrl),
|
||||
showDivider: false,
|
||||
),
|
||||
|
|
@ -185,15 +186,15 @@ class AboutPage extends StatelessWidget {
|
|||
),
|
||||
|
||||
// App info section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'App'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.aboutApp),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
_AboutSettingsItem(
|
||||
icon: Icons.info_outline,
|
||||
title: 'Version',
|
||||
title: context.l10n.aboutVersion,
|
||||
subtitle: 'v${AppInfo.version} (build ${AppInfo.buildNumber})',
|
||||
showDivider: false,
|
||||
),
|
||||
|
|
@ -300,7 +301,7 @@ class _AppHeaderCard extends StatelessWidget {
|
|||
const SizedBox(height: 16),
|
||||
// Description
|
||||
Text(
|
||||
'Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.',
|
||||
context.l10n.aboutAppDescription,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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/providers/theme_provider.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
|
@ -32,7 +33,7 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
|||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
flexibleSpace: _AppBarTitle(
|
||||
title: 'Appearance',
|
||||
title: context.l10n.appearanceTitle,
|
||||
topPadding: topPadding,
|
||||
),
|
||||
),
|
||||
|
|
@ -49,8 +50,8 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Color section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Color'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionColor),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
|
|
@ -58,8 +59,8 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
|||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.wallpaper,
|
||||
title: 'Dynamic Color',
|
||||
subtitle: 'Use colors from your wallpaper',
|
||||
title: context.l10n.appearanceDynamicColor,
|
||||
subtitle: context.l10n.appearanceDynamicColorSubtitle,
|
||||
value: themeSettings.useDynamicColor,
|
||||
onChanged: (value) => ref
|
||||
.read(themeProvider.notifier)
|
||||
|
|
@ -82,8 +83,8 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Theme section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Theme'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionTheme),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -96,8 +97,8 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
|||
if (Theme.of(context).brightness == Brightness.dark)
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.brightness_2,
|
||||
title: 'AMOLED Dark',
|
||||
subtitle: 'Pure black background',
|
||||
title: context.l10n.appearanceAmoledDark,
|
||||
subtitle: context.l10n.appearanceAmoledDarkSubtitle,
|
||||
value: themeSettings.useAmoled,
|
||||
onChanged: (value) =>
|
||||
ref.read(themeProvider.notifier).setUseAmoled(value),
|
||||
|
|
@ -108,8 +109,8 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Layout section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Layout'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionLayout),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -283,7 +284,7 @@ class _ThemePreviewCard extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
isDark ? 'Dark Mode' : 'Light Mode',
|
||||
isDark ? context.l10n.appearanceThemeDark : context.l10n.appearanceThemeLight,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
|
|
@ -451,21 +452,21 @@ class _ThemeModeSelector extends StatelessWidget {
|
|||
children: [
|
||||
_ThemeModeChip(
|
||||
icon: Icons.brightness_auto,
|
||||
label: 'System',
|
||||
label: context.l10n.appearanceThemeSystem,
|
||||
isSelected: currentMode == ThemeMode.system,
|
||||
onTap: () => onChanged(ThemeMode.system),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ThemeModeChip(
|
||||
icon: Icons.light_mode,
|
||||
label: 'Light',
|
||||
label: context.l10n.appearanceThemeLight,
|
||||
isSelected: currentMode == ThemeMode.light,
|
||||
onTap: () => onChanged(ThemeMode.light),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ThemeModeChip(
|
||||
icon: Icons.dark_mode,
|
||||
label: 'Dark',
|
||||
label: context.l10n.appearanceThemeDark,
|
||||
isSelected: currentMode == ThemeMode.dark,
|
||||
onTap: () => onChanged(ThemeMode.dark),
|
||||
),
|
||||
|
|
@ -575,7 +576,7 @@ class _HistoryViewSelector extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, bottom: 8),
|
||||
child: Text(
|
||||
'History View',
|
||||
context.l10n.appearanceHistoryView,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -585,14 +586,14 @@ class _HistoryViewSelector extends StatelessWidget {
|
|||
children: [
|
||||
_ViewModeChip(
|
||||
icon: Icons.view_list,
|
||||
label: 'List',
|
||||
label: context.l10n.appearanceHistoryViewList,
|
||||
isSelected: currentMode == 'list',
|
||||
onTap: () => onChanged('list'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ViewModeChip(
|
||||
icon: Icons.grid_view,
|
||||
label: 'Grid',
|
||||
label: context.l10n.appearanceHistoryViewGrid,
|
||||
isSelected: currentMode == 'grid',
|
||||
onTap: () => onChanged('grid'),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
|
@ -55,7 +56,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
bottom: 16,
|
||||
),
|
||||
title: Text(
|
||||
'Download',
|
||||
context.l10n.downloadTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio), // 20 -> 28
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -68,8 +69,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Service section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Service'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionService),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -85,17 +86,17 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Quality section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Audio Quality'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionAudioQuality),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.tune,
|
||||
title: 'Ask Before Download',
|
||||
title: context.l10n.downloadAskBeforeDownload,
|
||||
subtitle: isBuiltInService
|
||||
? 'Choose quality for each download'
|
||||
? context.l10n.downloadAskQualitySubtitle
|
||||
: 'Select a built-in service to enable',
|
||||
value: settings.askQualityBeforeDownload,
|
||||
// Not selected visually if extension is active
|
||||
|
|
@ -106,24 +107,24 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
if (!settings.askQualityBeforeDownload && isBuiltInService) ...[
|
||||
_QualityOption(
|
||||
title: 'FLAC Lossless',
|
||||
subtitle: '16-bit / 44.1kHz',
|
||||
title: context.l10n.qualityFlacLossless,
|
||||
subtitle: context.l10n.qualityFlacLosslessSubtitle,
|
||||
isSelected: settings.audioQuality == 'LOSSLESS',
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAudioQuality('LOSSLESS'),
|
||||
),
|
||||
_QualityOption(
|
||||
title: 'Hi-Res FLAC',
|
||||
subtitle: '24-bit / up to 96kHz',
|
||||
title: context.l10n.qualityHiResFlac,
|
||||
subtitle: context.l10n.qualityHiResFlacSubtitle,
|
||||
isSelected: settings.audioQuality == 'HI_RES',
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAudioQuality('HI_RES'),
|
||||
),
|
||||
_QualityOption(
|
||||
title: 'Hi-Res FLAC Max',
|
||||
subtitle: '24-bit / up to 192kHz',
|
||||
title: context.l10n.qualityHiResFlacMax,
|
||||
subtitle: context.l10n.qualityHiResFlacMaxSubtitle,
|
||||
isSelected: settings.audioQuality == 'HI_RES_LOSSLESS',
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
|
|
@ -159,15 +160,15 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// File settings section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'File Settings'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionFileSettings),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
icon: Icons.text_fields,
|
||||
title: 'Filename Format',
|
||||
title: context.l10n.downloadFilenameFormat,
|
||||
subtitle: settings.filenameFormat,
|
||||
onTap: () => _showFormatEditor(
|
||||
context,
|
||||
|
|
@ -177,17 +178,17 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
SettingsItem(
|
||||
icon: Icons.folder_outlined,
|
||||
title: 'Download Directory',
|
||||
title: context.l10n.downloadDirectory,
|
||||
subtitle: settings.downloadDirectory.isEmpty
|
||||
? (Platform.isIOS
|
||||
? 'App Documents Folder'
|
||||
? context.l10n.setupAppDocumentsFolder
|
||||
: 'Music/SpotiFLAC')
|
||||
: settings.downloadDirectory,
|
||||
onTap: () => _pickDirectory(context, ref),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.library_music_outlined,
|
||||
title: 'Separate Singles Folder',
|
||||
title: context.l10n.downloadSeparateSinglesFolder,
|
||||
subtitle: settings.separateSingles
|
||||
? 'Albums/ and Singles/ folders'
|
||||
: 'All files in same structure',
|
||||
|
|
@ -199,7 +200,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
if (settings.separateSingles)
|
||||
SettingsItem(
|
||||
icon: Icons.folder_outlined,
|
||||
title: 'Album Folder Structure',
|
||||
title: context.l10n.downloadAlbumFolderStructure,
|
||||
subtitle: _getAlbumFolderStructureLabel(settings.albumFolderStructure),
|
||||
onTap: () => _showAlbumFolderStructurePicker(
|
||||
context,
|
||||
|
|
@ -210,7 +211,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
if (!settings.separateSingles)
|
||||
SettingsItem(
|
||||
icon: Icons.create_new_folder_outlined,
|
||||
title: 'Folder Organization',
|
||||
title: context.l10n.downloadFolderOrganization,
|
||||
subtitle: _getFolderOrganizationLabel(
|
||||
settings.folderOrganization,
|
||||
),
|
||||
|
|
@ -254,8 +255,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder_outlined),
|
||||
title: const Text('Artist / Album'),
|
||||
subtitle: const Text('Albums/Artist Name/Album Name/'),
|
||||
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');
|
||||
|
|
@ -264,8 +265,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.calendar_today_outlined),
|
||||
title: const Text('Artist / [Year] Album'),
|
||||
subtitle: const Text('Albums/Artist Name/[2005] Album Name/'),
|
||||
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');
|
||||
|
|
@ -274,8 +275,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.album_outlined),
|
||||
title: const Text('Album Only'),
|
||||
subtitle: const Text('Albums/Album Name/'),
|
||||
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');
|
||||
|
|
@ -284,8 +285,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.event_outlined),
|
||||
title: const Text('[Year] Album Only'),
|
||||
subtitle: const Text('Albums/[2005] Album Name/'),
|
||||
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');
|
||||
|
|
@ -367,7 +368,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
Text(
|
||||
'Filename Format',
|
||||
context.l10n.filenameFormat,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
@ -433,7 +434,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
|
|
@ -441,7 +442,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
|
@ -460,7 +461,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text('Save Format'),
|
||||
child: Text(context.l10n.dialogSave),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -504,7 +505,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
'Download Location',
|
||||
context.l10n.setupDownloadLocationTitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
|
|
@ -513,7 +514,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.',
|
||||
context.l10n.setupDownloadLocationIosMessage,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -521,8 +522,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.folder_special, color: colorScheme.primary),
|
||||
title: const Text('App Documents Folder'),
|
||||
subtitle: const Text('Recommended - accessible via Files app'),
|
||||
title: Text(context.l10n.setupAppDocumentsFolder),
|
||||
subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle),
|
||||
trailing: Icon(Icons.check_circle, color: colorScheme.primary),
|
||||
onTap: () async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
|
|
@ -534,8 +535,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant),
|
||||
title: const Text('Choose from Files'),
|
||||
subtitle: const Text('Select iCloud or other location'),
|
||||
title: Text(context.l10n.setupChooseFromFiles),
|
||||
subtitle: Text(context.l10n.setupChooseFromFilesSubtitle),
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
// Note: iOS requires folder to have at least one file to be selectable
|
||||
|
|
@ -565,7 +566,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.',
|
||||
context.l10n.setupIosEmptyFolderWarning,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onTertiaryContainer,
|
||||
),
|
||||
|
|
@ -589,7 +590,7 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
case 'album':
|
||||
return 'By Album';
|
||||
case 'artist_album':
|
||||
return 'By Artist & Album';
|
||||
return 'Artist/Album';
|
||||
default:
|
||||
return 'None';
|
||||
}
|
||||
|
|
@ -629,15 +630,15 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
'Organize downloaded files into folders',
|
||||
context.l10n.folderOrganizationDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
_FolderOption(
|
||||
title: 'None',
|
||||
subtitle: 'All files in download folder',
|
||||
title: context.l10n.folderOrganizationNone,
|
||||
subtitle: context.l10n.folderOrganizationNoneSubtitle,
|
||||
example: 'SpotiFLAC/Track.flac',
|
||||
isSelected: current == 'none',
|
||||
onTap: () {
|
||||
|
|
@ -646,8 +647,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
},
|
||||
),
|
||||
_FolderOption(
|
||||
title: 'By Artist',
|
||||
subtitle: 'Separate folder for each artist',
|
||||
title: context.l10n.folderOrganizationByArtist,
|
||||
subtitle: context.l10n.folderOrganizationByArtistSubtitle,
|
||||
example: 'SpotiFLAC/Artist Name/Track.flac',
|
||||
isSelected: current == 'artist',
|
||||
onTap: () {
|
||||
|
|
@ -656,8 +657,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
},
|
||||
),
|
||||
_FolderOption(
|
||||
title: 'By Album',
|
||||
subtitle: 'Separate folder for each album',
|
||||
title: context.l10n.folderOrganizationByAlbum,
|
||||
subtitle: context.l10n.folderOrganizationByAlbumSubtitle,
|
||||
example: 'SpotiFLAC/Album Name/Track.flac',
|
||||
isSelected: current == 'album',
|
||||
onTap: () {
|
||||
|
|
@ -666,8 +667,8 @@ class DownloadSettingsPage extends ConsumerWidget {
|
|||
},
|
||||
),
|
||||
_FolderOption(
|
||||
title: 'By Artist & Album',
|
||||
subtitle: 'Nested folders for artist and album',
|
||||
title: context.l10n.folderOrganizationByArtistAlbum,
|
||||
subtitle: context.l10n.folderOrganizationByArtistAlbumSubtitle,
|
||||
example: 'SpotiFLAC/Artist/Album/Track.flac',
|
||||
isSelected: current == 'artist_album',
|
||||
onTap: () {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/store_provider.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
|
@ -186,12 +187,12 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_InfoRow(label: 'Author', value: extension.author),
|
||||
_InfoRow(label: 'ID', value: extension.id),
|
||||
_InfoRow(label: 'Version', value: 'v${extension.version}'),
|
||||
_InfoRow(label: context.l10n.extensionAuthor, value: extension.author),
|
||||
_InfoRow(label: context.l10n.extensionId, value: extension.id),
|
||||
_InfoRow(label: context.l10n.extensionsVersion(extension.version), value: ''),
|
||||
if (hasError && extension.errorMessage != null)
|
||||
_InfoRow(
|
||||
label: 'Error',
|
||||
label: context.l10n.extensionError,
|
||||
value: extension.errorMessage!,
|
||||
isError: true,
|
||||
),
|
||||
|
|
@ -202,50 +203,50 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
),
|
||||
|
||||
// Capabilities
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Capabilities'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionCapabilities),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
_CapabilityItem(
|
||||
icon: Icons.search,
|
||||
title: 'Metadata Provider',
|
||||
title: context.l10n.extensionMetadataProvider,
|
||||
enabled: extension.hasMetadataProvider,
|
||||
),
|
||||
_CapabilityItem(
|
||||
icon: Icons.download,
|
||||
title: 'Download Provider',
|
||||
title: context.l10n.extensionDownloadProvider,
|
||||
enabled: extension.hasDownloadProvider,
|
||||
),
|
||||
_CapabilityItem(
|
||||
icon: Icons.manage_search,
|
||||
title: 'Custom Search',
|
||||
title: context.l10n.extensionsSearchProvider,
|
||||
enabled: extension.hasCustomSearch,
|
||||
subtitle: extension.searchBehavior?.placeholder,
|
||||
),
|
||||
_CapabilityItem(
|
||||
icon: Icons.compare_arrows,
|
||||
title: 'Custom Track Matching',
|
||||
title: context.l10n.extensionCustomTrackMatching,
|
||||
enabled: extension.hasCustomMatching,
|
||||
subtitle: extension.trackMatching?.strategy != null
|
||||
? 'Strategy: ${extension.trackMatching!.strategy}'
|
||||
? context.l10n.extensionStrategy(extension.trackMatching!.strategy!)
|
||||
: null,
|
||||
),
|
||||
_CapabilityItem(
|
||||
icon: Icons.auto_fix_high,
|
||||
title: 'Post-Processing',
|
||||
title: context.l10n.extensionPostProcessing,
|
||||
enabled: extension.hasPostProcessing,
|
||||
subtitle: extension.postProcessing?.hooks.isNotEmpty == true
|
||||
? '${extension.postProcessing!.hooks.length} hook(s) available'
|
||||
? context.l10n.extensionHooksAvailable(extension.postProcessing!.hooks.length)
|
||||
: null,
|
||||
),
|
||||
_CapabilityItem(
|
||||
icon: Icons.link,
|
||||
title: 'URL Handler',
|
||||
title: context.l10n.extensionUrlHandler,
|
||||
enabled: extension.hasURLHandler,
|
||||
subtitle: extension.urlHandler?.patterns.isNotEmpty == true
|
||||
? '${extension.urlHandler!.patterns.length} pattern(s)'
|
||||
? context.l10n.extensionPatternsCount(extension.urlHandler!.patterns.length)
|
||||
: null,
|
||||
showDivider: false,
|
||||
),
|
||||
|
|
@ -257,8 +258,8 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
|
||||
// URL Handler Section (if extension handles URLs)
|
||||
if (extension.hasURLHandler && extension.urlHandler!.patterns.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'URL Handler'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionUrlHandler),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -273,8 +274,8 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
|
||||
// Quality Options Section (for download providers)
|
||||
if (extension.hasDownloadProvider && extension.qualityOptions.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Quality Options'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionQualityOptions),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -292,8 +293,8 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
|
||||
// Post-Processing Hooks (if available)
|
||||
if (extension.hasPostProcessing && extension.postProcessing!.hooks.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Post-Processing Hooks'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionPostProcessingHooks),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -311,8 +312,8 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
|
||||
// Permissions
|
||||
if (extension.permissions.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Permissions'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionPermissions),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -330,8 +331,8 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
|
||||
// Settings
|
||||
if (extension.settings.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Settings'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionSettings),
|
||||
),
|
||||
if (_isLoadingSettings)
|
||||
const SliverToBoxAdapter(
|
||||
|
|
@ -364,7 +365,7 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
child: OutlinedButton.icon(
|
||||
onPressed: () => _confirmRemove(context),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: const Text('Remove Extension'),
|
||||
label: Text(context.l10n.extensionRemoveButton),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: colorScheme.error,
|
||||
side: BorderSide(color: colorScheme.error),
|
||||
|
|
@ -398,22 +399,21 @@ class _ExtensionDetailPageState extends ConsumerState<ExtensionDetailPage> {
|
|||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Remove Extension'),
|
||||
content: const Text(
|
||||
'Are you sure you want to remove this extension? '
|
||||
'This action cannot be undone.',
|
||||
title: Text(context.l10n.dialogRemoveExtension),
|
||||
content: Text(
|
||||
context.l10n.dialogRemoveExtensionMessage,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: colorScheme.error,
|
||||
),
|
||||
child: const Text('Remove'),
|
||||
child: Text(context.l10n.dialogRemove),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -725,7 +725,7 @@ class _SettingItem extends StatelessWidget {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
|
|
@ -735,7 +735,7 @@ class _SettingItem extends StatelessWidget {
|
|||
onChanged(newValue);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
child: Text(context.l10n.dialogSave),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/extension_detail_page.dart';
|
||||
|
|
@ -74,7 +75,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
||||
title: Text(
|
||||
'Extensions',
|
||||
context.l10n.extensionsTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -123,8 +124,8 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
),
|
||||
|
||||
// Provider Priority
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Provider Priority'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionsProviderPrioritySection),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -137,8 +138,8 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
),
|
||||
|
||||
// Installed Extensions
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Installed Extensions'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionsInstalledSection),
|
||||
),
|
||||
|
||||
if (extState.extensions.isEmpty && !extState.isLoading)
|
||||
|
|
@ -160,14 +161,14 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'No extensions installed',
|
||||
context.l10n.extensionsNoExtensions,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Install .spotiflac-ext files to add new providers',
|
||||
context.l10n.extensionsNoExtensionsSubtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -209,7 +210,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
child: FilledButton.icon(
|
||||
onPressed: _installExtension,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Install Extension'),
|
||||
label: Text(context.l10n.extensionsInstallButton),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -236,8 +237,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Extensions can add new metadata and download providers. '
|
||||
'Only install extensions from trusted sources.',
|
||||
context.l10n.extensionsInfoTip,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onTertiaryContainer,
|
||||
),
|
||||
|
|
@ -266,8 +266,8 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
if (!file.path!.endsWith('.spotiflac-ext')) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please select a .spotiflac-ext file'),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarSelectExtFile),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -282,7 +282,7 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
|||
final extState = ref.read(extensionProvider);
|
||||
String message;
|
||||
if (success) {
|
||||
message = 'Extension installed successfully';
|
||||
message = context.l10n.extensionsInstalledSuccess;
|
||||
} else {
|
||||
// Parse friendly error message
|
||||
message = _getFriendlyErrorMessage(extState.error);
|
||||
|
|
@ -404,8 +404,8 @@ class _ExtensionItem extends StatelessWidget {
|
|||
const SizedBox(height: 2),
|
||||
Text(
|
||||
hasError
|
||||
? extension.errorMessage ?? 'Error loading extension'
|
||||
: 'v${extension.version} by ${extension.author}',
|
||||
? extension.errorMessage ?? context.l10n.extensionsErrorLoading
|
||||
: 'v${extension.version} ${context.l10n.extensionsAuthor(extension.author)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: hasError
|
||||
? colorScheme.error
|
||||
|
|
@ -474,7 +474,7 @@ class _DownloadPriorityItem extends ConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Download Priority',
|
||||
context.l10n.extensionsDownloadPriority,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: hasDownloadExtensions
|
||||
? null
|
||||
|
|
@ -484,8 +484,8 @@ class _DownloadPriorityItem extends ConsumerWidget {
|
|||
const SizedBox(height: 2),
|
||||
Text(
|
||||
hasDownloadExtensions
|
||||
? 'Set download service order'
|
||||
: 'No extensions with download provider',
|
||||
? context.l10n.extensionsDownloadPrioritySubtitle
|
||||
: context.l10n.extensionsNoDownloadProvider,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -543,7 +543,7 @@ class _MetadataPriorityItem extends ConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Metadata Priority',
|
||||
context.l10n.extensionsMetadataPriority,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: hasMetadataExtensions
|
||||
? null
|
||||
|
|
@ -553,8 +553,8 @@ class _MetadataPriorityItem extends ConsumerWidget {
|
|||
const SizedBox(height: 2),
|
||||
Text(
|
||||
hasMetadataExtensions
|
||||
? 'Set search & metadata source order'
|
||||
: 'No extensions with metadata provider',
|
||||
? context.l10n.extensionsMetadataPrioritySubtitle
|
||||
: context.l10n.extensionsNoMetadataProvider,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -590,7 +590,7 @@ class _SearchProviderSelector extends ConsumerWidget {
|
|||
.toList();
|
||||
|
||||
// Get current provider name
|
||||
String currentProviderName = 'Default (Deezer/Spotify)';
|
||||
String currentProviderName = context.l10n.extensionDefaultProvider;
|
||||
if (settings.searchProvider != null && settings.searchProvider!.isNotEmpty) {
|
||||
final ext = searchProviders.where((e) => e.id == settings.searchProvider).firstOrNull;
|
||||
currentProviderName = ext?.displayName ?? settings.searchProvider!;
|
||||
|
|
@ -619,7 +619,7 @@ class _SearchProviderSelector extends ConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Search Provider',
|
||||
context.l10n.extensionsSearchProvider,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: searchProviders.isEmpty
|
||||
? colorScheme.outline
|
||||
|
|
@ -629,7 +629,7 @@ class _SearchProviderSelector extends ConsumerWidget {
|
|||
const SizedBox(height: 2),
|
||||
Text(
|
||||
searchProviders.isEmpty
|
||||
? 'No extensions with custom search'
|
||||
? context.l10n.extensionsNoCustomSearch
|
||||
: currentProviderName,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
|
|
@ -674,7 +674,7 @@ class _SearchProviderSelector extends ConsumerWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
'Search Provider',
|
||||
ctx.l10n.extensionsSearchProvider,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
@ -683,7 +683,7 @@ class _SearchProviderSelector extends ConsumerWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
'Choose which service to use for searching tracks',
|
||||
ctx.l10n.extensionsSearchProviderDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -692,8 +692,8 @@ class _SearchProviderSelector extends ConsumerWidget {
|
|||
// Default option
|
||||
ListTile(
|
||||
leading: Icon(Icons.music_note, color: colorScheme.primary),
|
||||
title: const Text('Default (Deezer/Spotify)'),
|
||||
subtitle: const Text('Use built-in search'),
|
||||
title: Text(ctx.l10n.extensionDefaultProvider),
|
||||
subtitle: Text(ctx.l10n.extensionDefaultProviderSubtitle),
|
||||
trailing: (settings.searchProvider == null || settings.searchProvider!.isEmpty)
|
||||
? Icon(Icons.check_circle, color: colorScheme.primary)
|
||||
: Icon(Icons.circle_outlined, color: colorScheme.outline),
|
||||
|
|
@ -706,7 +706,7 @@ class _SearchProviderSelector extends ConsumerWidget {
|
|||
...searchProviders.map((ext) => ListTile(
|
||||
leading: Icon(Icons.extension, color: colorScheme.secondary),
|
||||
title: Text(ext.displayName),
|
||||
subtitle: Text(ext.searchBehavior?.placeholder ?? 'Custom search'),
|
||||
subtitle: Text(ext.searchBehavior?.placeholder ?? ctx.l10n.extensionsCustomSearch),
|
||||
trailing: settings.searchProvider == ext.id
|
||||
? Icon(Icons.check_circle, color: colorScheme.primary)
|
||||
: Icon(Icons.circle_outlined, color: colorScheme.outline),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus;
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ class _LogScreenState extends State<LogScreen> {
|
|||
Clipboard.setData(ClipboardData(text: logs));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Logs copied to clipboard'),
|
||||
content: Text(context.l10n.logCopied),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
duration: const Duration(seconds: 2),
|
||||
|
|
@ -84,19 +85,19 @@ class _LogScreenState extends State<LogScreen> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear Logs'),
|
||||
content: const Text('Are you sure you want to clear all logs?'),
|
||||
title: Text(context.l10n.logClearLogsTitle),
|
||||
content: Text(context.l10n.logClearLogsMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
LogBuffer().clear();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Clear'),
|
||||
child: Text(context.l10n.dialogClear),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -166,19 +167,19 @@ class _LogScreenState extends State<LogScreen> {
|
|||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'share',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.share),
|
||||
title: Text('Share logs'),
|
||||
leading: const Icon(Icons.share),
|
||||
title: Text(context.l10n.logShareLogs),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'clear',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete_outline),
|
||||
title: Text('Clear logs'),
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: Text(context.l10n.logClearLogs),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
|
|
@ -195,7 +196,7 @@ class _LogScreenState extends State<LogScreen> {
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
||||
title: Text(
|
||||
'Logs',
|
||||
context.l10n.logTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -208,8 +209,8 @@ class _LogScreenState extends State<LogScreen> {
|
|||
),
|
||||
|
||||
// Filter section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Filter'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.logFilterSection),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -225,10 +226,10 @@ class _LogScreenState extends State<LogScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Level', style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(context.l10n.logFilterLevel, style: Theme.of(context).textTheme.bodyLarge),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Filter logs by severity',
|
||||
context.l10n.logFilterBySeverity,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -279,7 +280,7 @@ class _LogScreenState extends State<LogScreen> {
|
|||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search logs...',
|
||||
hintText: context.l10n.logSearchHint,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
|
|
@ -316,7 +317,9 @@ class _LogScreenState extends State<LogScreen> {
|
|||
// Log entries section
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(
|
||||
title: 'Entries (${logs.length}${_selectedLevel != 'ALL' || _searchQuery.isNotEmpty ? ' filtered' : ''})',
|
||||
title: _selectedLevel != 'ALL' || _searchQuery.isNotEmpty
|
||||
? context.l10n.logEntriesFiltered(logs.length)
|
||||
: context.l10n.logEntries(logs.length),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
@ -342,14 +345,14 @@ class _LogScreenState extends State<LogScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No logs yet',
|
||||
context.l10n.logNoLogsYet,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Logs will appear here as you use the app',
|
||||
context.l10n.logNoLogsYetSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
|
||||
class MetadataProviderPriorityPage extends ConsumerStatefulWidget {
|
||||
|
|
@ -81,7 +82,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState<MetadataProviderP
|
|||
if (_hasChanges)
|
||||
TextButton(
|
||||
onPressed: _saveChanges,
|
||||
child: const Text('Save'),
|
||||
child: Text(context.l10n.dialogSave),
|
||||
),
|
||||
],
|
||||
flexibleSpace: LayoutBuilder(
|
||||
|
|
@ -96,7 +97,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState<MetadataProviderP
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
||||
title: Text(
|
||||
'Metadata Priority',
|
||||
context.l10n.metadataProviderPriorityTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -113,8 +114,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState<MetadataProviderP
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Drag to reorder metadata providers. The app will try providers '
|
||||
'from top to bottom when searching for tracks and fetching metadata.',
|
||||
context.l10n.metadataProviderPriorityDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -166,8 +166,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState<MetadataProviderP
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Deezer has no rate limits and is recommended as primary. '
|
||||
'Spotify may rate limit after many requests.',
|
||||
context.l10n.metadataProviderPriorityInfo,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onTertiaryContainer,
|
||||
),
|
||||
|
|
@ -190,16 +189,16 @@ class _MetadataProviderPriorityPageState extends ConsumerState<MetadataProviderP
|
|||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Discard Changes?'),
|
||||
content: const Text('You have unsaved changes. Do you want to discard them?'),
|
||||
title: Text(context.l10n.dialogDiscardChanges),
|
||||
content: Text(context.l10n.dialogUnsavedChanges),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Discard'),
|
||||
child: Text(context.l10n.dialogDiscard),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -214,7 +213,7 @@ class _MetadataProviderPriorityPageState extends ConsumerState<MetadataProviderP
|
|||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Metadata provider priority saved')),
|
||||
SnackBar(content: Text(context.l10n.snackbarMetadataProviderSaved)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -246,7 +245,7 @@ class _MetadataProviderItem extends StatelessWidget {
|
|||
)
|
||||
: colorScheme.surfaceContainerHigh;
|
||||
|
||||
final info = _getProviderInfo(provider);
|
||||
final info = _getProviderInfo(context, provider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
|
|
@ -323,20 +322,20 @@ class _MetadataProviderItem extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
_MetadataProviderInfo _getProviderInfo(String provider) {
|
||||
_MetadataProviderInfo _getProviderInfo(BuildContext context, String provider) {
|
||||
switch (provider) {
|
||||
case 'deezer':
|
||||
return _MetadataProviderInfo(
|
||||
name: 'Deezer',
|
||||
icon: Icons.album,
|
||||
description: 'No rate limits',
|
||||
description: context.l10n.metadataNoRateLimits,
|
||||
isBuiltIn: true,
|
||||
);
|
||||
case 'spotify':
|
||||
return _MetadataProviderInfo(
|
||||
name: 'Spotify',
|
||||
icon: Icons.music_note,
|
||||
description: 'May rate limit',
|
||||
description: context.l10n.metadataMayRateLimit,
|
||||
isBuiltIn: true,
|
||||
);
|
||||
default:
|
||||
|
|
@ -344,7 +343,7 @@ class _MetadataProviderItem extends StatelessWidget {
|
|||
return _MetadataProviderInfo(
|
||||
name: provider,
|
||||
icon: Icons.extension,
|
||||
description: 'Extension',
|
||||
description: context.l10n.providerExtension,
|
||||
isBuiltIn: false,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
|
|
@ -50,7 +51,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
bottom: 16,
|
||||
),
|
||||
title: Text(
|
||||
'Options',
|
||||
context.l10n.optionsTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio), // 20 -> 28
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -63,8 +64,8 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Search Source section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Search Source'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionSearchSource),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -93,7 +94,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Spotify requires your own API credentials. Get them free from developer.spotify.com',
|
||||
context.l10n.optionsSpotifyWarning,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
fontSize: 12,
|
||||
|
|
@ -107,10 +108,10 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
SettingsItem(
|
||||
icon: Icons.key,
|
||||
title: 'Spotify Credentials',
|
||||
title: context.l10n.optionsSpotifyCredentials,
|
||||
subtitle: settings.spotifyClientId.isNotEmpty
|
||||
? 'Client ID: ${settings.spotifyClientId.length > 8 ? '${settings.spotifyClientId.substring(0, 8)}...' : settings.spotifyClientId}'
|
||||
: 'Required - tap to configure',
|
||||
? context.l10n.optionsSpotifyCredentialsConfigured(settings.spotifyClientId.length > 8 ? settings.spotifyClientId.substring(0, 8) : settings.spotifyClientId)
|
||||
: context.l10n.optionsSpotifyCredentialsRequired,
|
||||
onTap: () =>
|
||||
_showSpotifyCredentialsDialog(context, ref, settings),
|
||||
trailing: Icon(
|
||||
|
|
@ -130,16 +131,16 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Download options section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Download'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionDownload),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.sync,
|
||||
title: 'Auto Fallback',
|
||||
subtitle: 'Try other services if download fails',
|
||||
title: context.l10n.optionsAutoFallback,
|
||||
subtitle: context.l10n.optionsAutoFallbackSubtitle,
|
||||
value: settings.autoFallback,
|
||||
onChanged: (v) =>
|
||||
ref.read(settingsProvider.notifier).setAutoFallback(v),
|
||||
|
|
@ -147,10 +148,10 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
if (hasExtensions)
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.extension,
|
||||
title: 'Use Extension Providers',
|
||||
title: context.l10n.optionsUseExtensionProviders,
|
||||
subtitle: settings.useExtensionProviders
|
||||
? 'Extensions will be tried first'
|
||||
: 'Using built-in providers only',
|
||||
? context.l10n.optionsUseExtensionProvidersOn
|
||||
: context.l10n.optionsUseExtensionProvidersOff,
|
||||
value: settings.useExtensionProviders,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
|
|
@ -158,16 +159,16 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.lyrics,
|
||||
title: 'Embed Lyrics',
|
||||
subtitle: 'Embed synced lyrics into FLAC files',
|
||||
title: context.l10n.optionsEmbedLyrics,
|
||||
subtitle: context.l10n.optionsEmbedLyricsSubtitle,
|
||||
value: settings.embedLyrics,
|
||||
onChanged: (v) =>
|
||||
ref.read(settingsProvider.notifier).setEmbedLyrics(v),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.image,
|
||||
title: 'Max Quality Cover',
|
||||
subtitle: 'Download highest resolution cover art',
|
||||
title: context.l10n.optionsMaxQualityCover,
|
||||
subtitle: context.l10n.optionsMaxQualityCoverSubtitle,
|
||||
value: settings.maxQualityCover,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
|
|
@ -179,8 +180,8 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Performance section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Performance'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionPerformance),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
|
|
@ -196,16 +197,16 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// App section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'App'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionApp),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.store,
|
||||
title: 'Extension Store',
|
||||
subtitle: 'Show Store tab in navigation',
|
||||
title: context.l10n.optionsExtensionStore,
|
||||
subtitle: context.l10n.optionsExtensionStoreSubtitle,
|
||||
value: settings.showExtensionStore,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
|
|
@ -213,8 +214,8 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.system_update,
|
||||
title: 'Check for Updates',
|
||||
subtitle: 'Notify when new version is available',
|
||||
title: context.l10n.optionsCheckUpdates,
|
||||
subtitle: context.l10n.optionsCheckUpdatesSubtitle,
|
||||
value: settings.checkForUpdates,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
|
|
@ -230,16 +231,16 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Data section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Data'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionData),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
icon: Icons.delete_forever,
|
||||
title: 'Clear Download History',
|
||||
subtitle: 'Remove all downloaded tracks from history',
|
||||
title: context.l10n.optionsClearHistory,
|
||||
subtitle: context.l10n.optionsClearHistorySubtitle,
|
||||
onTap: () =>
|
||||
_showClearHistoryDialog(context, ref, colorScheme),
|
||||
showDivider: false,
|
||||
|
|
@ -249,18 +250,18 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
|
||||
// Debug section
|
||||
const SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Debug'),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionDebug),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.bug_report,
|
||||
title: 'Detailed Logging',
|
||||
title: context.l10n.optionsDetailedLogging,
|
||||
subtitle: settings.enableLogging
|
||||
? 'Detailed logs are being recorded'
|
||||
: 'Enable for bug reports',
|
||||
? context.l10n.optionsDetailedLoggingOn
|
||||
: context.l10n.optionsDetailedLoggingOff,
|
||||
value: settings.enableLogging,
|
||||
onChanged: (v) =>
|
||||
ref.read(settingsProvider.notifier).setEnableLogging(v),
|
||||
|
|
@ -285,14 +286,14 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Clear History'),
|
||||
content: const Text(
|
||||
'Are you sure you want to clear all download history? This cannot be undone.',
|
||||
title: Text(context.l10n.dialogClearHistoryTitle),
|
||||
content: Text(
|
||||
context.l10n.dialogClearHistoryMessage,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
|
|
@ -300,9 +301,9 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('History cleared')));
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarHistoryCleared)));
|
||||
},
|
||||
child: Text('Clear', style: TextStyle(color: colorScheme.error)),
|
||||
child: Text(context.l10n.dialogClear, style: TextStyle(color: colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -353,7 +354,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
Text(
|
||||
'Spotify Credentials',
|
||||
context.l10n.credentialsTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
@ -361,7 +362,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Enter your Client ID and Secret to use your own Spotify application quota.',
|
||||
context.l10n.credentialsDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -373,8 +374,8 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
TextField(
|
||||
controller: clientIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Client ID',
|
||||
hintText: 'Paste Client ID',
|
||||
labelText: context.l10n.credentialsClientId,
|
||||
hintText: context.l10n.credentialsClientIdHint,
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
|
|
@ -412,8 +413,8 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
controller: clientSecretController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Client Secret',
|
||||
hintText: 'Paste Client Secret',
|
||||
labelText: context.l10n.credentialsClientSecret,
|
||||
hintText: context.l10n.credentialsClientSecretHint,
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
|
|
@ -458,12 +459,12 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
.setSpotifyCredentials(clientId, clientSecret);
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Credentials saved')),
|
||||
SnackBar(content: Text(context.l10n.snackbarCredentialsSaved)),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please fill all fields'),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.snackbarFillAllFields),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -474,9 +475,9 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Save Credentials',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
child: Text(
|
||||
context.l10n.actionSaveCredentials,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
@ -489,14 +490,14 @@ class OptionsSettingsPage extends ConsumerWidget {
|
|||
.clearSpotifyCredentials();
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Credentials cleared')),
|
||||
SnackBar(content: Text(context.l10n.snackbarCredentialsCleared)),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colorScheme.error,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text('Remove Credentials'),
|
||||
child: Text(context.l10n.actionRemoveCredentials),
|
||||
),
|
||||
],
|
||||
|
||||
|
|
@ -540,14 +541,14 @@ class _ConcurrentDownloadsItem extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Concurrent Downloads',
|
||||
context.l10n.optionsConcurrentDownloads,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
currentValue == 1
|
||||
? 'Sequential (1 at a time)'
|
||||
: '$currentValue parallel downloads',
|
||||
? context.l10n.optionsConcurrentSequential
|
||||
: context.l10n.optionsConcurrentParallel(currentValue),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -590,7 +591,7 @@ class _ConcurrentDownloadsItem extends StatelessWidget {
|
|||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Parallel downloads may trigger rate limiting',
|
||||
context.l10n.optionsConcurrentWarning,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: colorScheme.error),
|
||||
|
|
@ -682,14 +683,14 @@ class _UpdateChannelSelector extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Update Channel',
|
||||
context.l10n.optionsUpdateChannel,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
currentChannel == 'preview'
|
||||
? 'Get preview releases'
|
||||
: 'Stable releases only',
|
||||
? context.l10n.optionsUpdateChannelPreview
|
||||
: context.l10n.optionsUpdateChannelStable,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -703,13 +704,13 @@ class _UpdateChannelSelector extends StatelessWidget {
|
|||
Row(
|
||||
children: [
|
||||
_ChannelChip(
|
||||
label: 'Stable',
|
||||
label: context.l10n.channelStable,
|
||||
isSelected: currentChannel == 'stable',
|
||||
onTap: () => onChanged('stable'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ChannelChip(
|
||||
label: 'Preview',
|
||||
label: context.l10n.channelPreview,
|
||||
isSelected: currentChannel == 'preview',
|
||||
onTap: () => onChanged('preview'),
|
||||
),
|
||||
|
|
@ -726,7 +727,7 @@ class _UpdateChannelSelector extends StatelessWidget {
|
|||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Preview may contain bugs or incomplete features',
|
||||
context.l10n.optionsUpdateChannelWarning,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -823,7 +824,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Primary Provider',
|
||||
context.l10n.optionsPrimaryProvider,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
|
|
@ -831,8 +832,8 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
|||
const SizedBox(height: 4),
|
||||
Text(
|
||||
hasExtensionSearch
|
||||
? 'Using extension: $extensionName'
|
||||
: 'Service used when searching by track name.',
|
||||
? context.l10n.optionsUsingExtension(extensionName!)
|
||||
: context.l10n.optionsPrimaryProviderSubtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: hasExtensionSearch
|
||||
? colorScheme.primary
|
||||
|
|
@ -883,7 +884,7 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
|||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Tap Deezer or Spotify to switch back from extension',
|
||||
context.l10n.optionsSwitchBack,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
|
||||
class ProviderPriorityPage extends ConsumerStatefulWidget {
|
||||
|
|
@ -82,7 +83,7 @@ class _ProviderPriorityPageState extends ConsumerState<ProviderPriorityPage> {
|
|||
if (_hasChanges)
|
||||
TextButton(
|
||||
onPressed: _saveChanges,
|
||||
child: const Text('Save'),
|
||||
child: Text(context.l10n.dialogSave),
|
||||
),
|
||||
],
|
||||
flexibleSpace: LayoutBuilder(
|
||||
|
|
@ -97,7 +98,7 @@ class _ProviderPriorityPageState extends ConsumerState<ProviderPriorityPage> {
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
|
||||
title: Text(
|
||||
'Provider Priority',
|
||||
context.l10n.providerPriorityTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (8 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -114,8 +115,7 @@ class _ProviderPriorityPageState extends ConsumerState<ProviderPriorityPage> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Drag to reorder download providers. The app will try providers '
|
||||
'from top to bottom when downloading tracks.',
|
||||
context.l10n.providerPriorityDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -167,8 +167,7 @@ class _ProviderPriorityPageState extends ConsumerState<ProviderPriorityPage> {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'If a track is not available on the first provider, '
|
||||
'the app will automatically try the next one.',
|
||||
context.l10n.providerPriorityInfo,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onTertiaryContainer,
|
||||
),
|
||||
|
|
@ -191,16 +190,16 @@ class _ProviderPriorityPageState extends ConsumerState<ProviderPriorityPage> {
|
|||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Discard Changes?'),
|
||||
content: const Text('You have unsaved changes. Do you want to discard them?'),
|
||||
title: Text(context.l10n.dialogDiscardChanges),
|
||||
content: Text(context.l10n.dialogUnsavedChanges),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Discard'),
|
||||
child: Text(context.l10n.dialogDiscard),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -215,7 +214,7 @@ class _ProviderPriorityPageState extends ConsumerState<ProviderPriorityPage> {
|
|||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Provider priority saved')),
|
||||
SnackBar(content: Text(context.l10n.snackbarProviderPrioritySaved)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -304,7 +303,7 @@ class _ProviderItem extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Text(
|
||||
info.isBuiltIn ? 'Built-in' : 'Extension',
|
||||
info.isBuiltIn ? context.l10n.providerBuiltIn : context.l10n.providerExtension,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
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/extensions_page.dart';
|
||||
|
|
@ -41,7 +42,7 @@ class SettingsTab extends ConsumerWidget {
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
title: Text(
|
||||
'Settings',
|
||||
context.l10n.settingsTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (14 * expandRatio), // 20 -> 34
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -55,57 +56,67 @@ class SettingsTab extends ConsumerWidget {
|
|||
|
||||
// First group: Appearance & Download
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
children: [
|
||||
SettingsItem(
|
||||
icon: Icons.palette_outlined,
|
||||
title: 'Appearance',
|
||||
subtitle: 'Theme, colors, display',
|
||||
onTap: () =>
|
||||
_navigateTo(context, const AppearanceSettingsPage()),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.download_outlined,
|
||||
title: 'Download',
|
||||
subtitle: 'Service, quality, filename format',
|
||||
onTap: () => _navigateTo(context, const DownloadSettingsPage()),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.tune_outlined,
|
||||
title: 'Options',
|
||||
subtitle: 'Fallback, lyrics, cover art, updates',
|
||||
onTap: () => _navigateTo(context, const OptionsSettingsPage()),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.extension_outlined,
|
||||
title: 'Extensions',
|
||||
subtitle: 'Manage download providers',
|
||||
onTap: () => _navigateTo(context, const ExtensionsPage()),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return SettingsGroup(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
children: [
|
||||
SettingsItem(
|
||||
icon: Icons.palette_outlined,
|
||||
title: l10n.settingsAppearance,
|
||||
subtitle: l10n.settingsAppearanceSubtitle,
|
||||
onTap: () =>
|
||||
_navigateTo(context, const AppearanceSettingsPage()),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.download_outlined,
|
||||
title: l10n.settingsDownload,
|
||||
subtitle: l10n.settingsDownloadSubtitle,
|
||||
onTap: () => _navigateTo(context, const DownloadSettingsPage()),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.tune_outlined,
|
||||
title: l10n.settingsOptions,
|
||||
subtitle: l10n.settingsOptionsSubtitle,
|
||||
onTap: () => _navigateTo(context, const OptionsSettingsPage()),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.extension_outlined,
|
||||
title: l10n.settingsExtensions,
|
||||
subtitle: l10n.settingsExtensionsSubtitle,
|
||||
onTap: () => _navigateTo(context, const ExtensionsPage()),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Second group: Logs & About
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
icon: Icons.article_outlined,
|
||||
title: 'Logs',
|
||||
subtitle: 'View app logs for debugging',
|
||||
onTap: () => _navigateTo(context, const LogScreen()),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.info_outline,
|
||||
title: 'About',
|
||||
subtitle: 'Version ${AppInfo.version}, credits, GitHub',
|
||||
onTap: () => _navigateTo(context, const AboutPage()),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
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,
|
||||
title: l10n.settingsAbout,
|
||||
subtitle: '${l10n.aboutVersion} ${AppInfo.version}',
|
||||
onTap: () => _navigateTo(context, const AboutPage()),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
|
||||
class SetupScreen extends ConsumerStatefulWidget {
|
||||
const SetupScreen({super.key});
|
||||
|
|
@ -123,19 +124,19 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
final shouldOpen = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Storage Access Required'),
|
||||
content: const Text(
|
||||
'SpotiFLAC needs "All files access" permission to save music files to your chosen folder.\n\n'
|
||||
'Please enable "Allow access to manage all files" in the next screen.',
|
||||
title: Text(context.l10n.setupStorageAccessRequired),
|
||||
content: Text(
|
||||
'${context.l10n.setupStorageAccessMessage}\n\n'
|
||||
'${context.l10n.setupAllowAccessToManageFiles}',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Open Settings'),
|
||||
child: Text(context.l10n.setupOpenSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -166,19 +167,19 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
final shouldOpen = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Storage Access Required'),
|
||||
content: const Text(
|
||||
'Android 11+ requires "All files access" permission to save music files.\n\n'
|
||||
'Please enable "Allow access to manage all files" in the next screen.',
|
||||
title: Text(context.l10n.setupStorageAccessRequired),
|
||||
content: Text(
|
||||
'${context.l10n.setupStorageAccessMessageAndroid11}\n\n'
|
||||
'${context.l10n.setupAllowAccessToManageFiles}',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Open Settings'),
|
||||
child: Text(context.l10n.setupOpenSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -211,7 +212,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Permission denied. Please grant all permissions to continue.')),
|
||||
SnackBar(content: Text(context.l10n.setupPermissionDeniedMessage)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -256,22 +257,21 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('$permissionType Permission Required'),
|
||||
title: Text(context.l10n.setupPermissionRequired(permissionType)),
|
||||
content: Text(
|
||||
'$permissionType permission is required for the best experience. '
|
||||
'Please grant permission in app settings.',
|
||||
context.l10n.setupPermissionRequiredMessage(permissionType),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
openAppSettings();
|
||||
},
|
||||
child: const Text('Open Settings'),
|
||||
child: Text(context.l10n.setupOpenSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -288,7 +288,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
} else {
|
||||
// Android: Use file picker
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: 'Select Download Folder',
|
||||
dialogTitle: context.l10n.setupSelectDownloadFolder,
|
||||
);
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
|
|
@ -299,11 +299,11 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
final useDefault = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Use Default Folder?'),
|
||||
content: Text('No folder selected. Would you like to use the default Music folder?\n\n$defaultDir'),
|
||||
title: Text(context.l10n.setupUseDefaultFolder),
|
||||
content: Text('${context.l10n.setupNoFolderSelected}\n\n$defaultDir'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Use Default')),
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(context.l10n.dialogCancel)),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.setupUseDefault)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -333,19 +333,19 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text('Download Location', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
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(
|
||||
'On iOS, downloads are saved to the app\'s Documents folder which is accessible via the Files app.',
|
||||
context.l10n.setupDownloadLocationIosMessage,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.folder_special, color: colorScheme.primary),
|
||||
title: const Text('App Documents Folder'),
|
||||
subtitle: const Text('Recommended - accessible via Files app'),
|
||||
title: Text(context.l10n.setupAppDocumentsFolder),
|
||||
subtitle: Text(context.l10n.setupAppDocumentsFolderSubtitle),
|
||||
trailing: Icon(Icons.check_circle, color: colorScheme.primary),
|
||||
onTap: () async {
|
||||
final dir = await _getDefaultDirectory();
|
||||
|
|
@ -355,8 +355,8 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.cloud, color: colorScheme.onSurfaceVariant),
|
||||
title: const Text('Choose from Files'),
|
||||
subtitle: const Text('Select iCloud or other location'),
|
||||
title: Text(context.l10n.setupChooseFromFiles),
|
||||
subtitle: Text(context.l10n.setupChooseFromFilesSubtitle),
|
||||
onTap: () async {
|
||||
Navigator.pop(ctx);
|
||||
// Note: iOS requires folder to have at least one file to be selectable
|
||||
|
|
@ -380,7 +380,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'iOS limitation: Empty folders cannot be selected. Create a file inside first or use App Documents.',
|
||||
context.l10n.setupIosEmptyFolderWarning,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer),
|
||||
),
|
||||
),
|
||||
|
|
@ -486,16 +486,16 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
ClipRRect(
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Image.asset('assets/images/logo.png', width: 96, height: 96),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('SpotiFLAC',
|
||||
Text(context.l10n.appName,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold, color: colorScheme.primary)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Download Spotify tracks in FLAC',
|
||||
Text(context.l10n.setupDownloadInFlac,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
|
|
@ -529,8 +529,8 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
|
||||
Widget _buildStepIndicator(ColorScheme colorScheme) {
|
||||
final steps = _androidSdkVersion >= 33
|
||||
? ['Storage', 'Notification', 'Folder', 'Spotify']
|
||||
: ['Permission', 'Folder', 'Spotify'];
|
||||
? [context.l10n.setupStepStorage, context.l10n.setupStepNotification, context.l10n.setupStepFolder, context.l10n.setupStepSpotify]
|
||||
: [context.l10n.setupStepPermission, context.l10n.setupStepFolder, context.l10n.setupStepSpotify];
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -653,7 +653,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
_storagePermissionGranted ? 'Storage Permission Granted!' : 'Storage Permission Required',
|
||||
_storagePermissionGranted ? context.l10n.setupStorageGranted : context.l10n.setupStorageRequired,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -662,8 +662,8 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
_storagePermissionGranted
|
||||
? 'You can now proceed to the next step.'
|
||||
: 'SpotiFLAC needs storage access to save downloaded music files to your device.',
|
||||
? context.l10n.setupProceedToNextStep
|
||||
: context.l10n.setupStorageDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -676,7 +676,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
? SizedBox(width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary))
|
||||
: const Icon(Icons.security_rounded),
|
||||
label: const Text('Grant Permission'),
|
||||
label: Text(context.l10n.setupGrantPermission),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
|
|
@ -707,7 +707,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
_notificationPermissionGranted ? 'Notification Permission Granted!' : 'Enable Notifications',
|
||||
_notificationPermissionGranted ? context.l10n.setupNotificationGranted : context.l10n.setupNotificationEnable,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -716,8 +716,8 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
_notificationPermissionGranted
|
||||
? 'You will receive download progress notifications.'
|
||||
: 'Get notified about download progress and completion. This helps you track downloads when the app is in background.',
|
||||
? context.l10n.setupNotificationProgressDescription
|
||||
: context.l10n.setupNotificationBackgroundDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -730,7 +730,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
? SizedBox(width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary))
|
||||
: const Icon(Icons.notifications_active_rounded),
|
||||
label: const Text('Enable Notifications'),
|
||||
label: Text(context.l10n.setupEnableNotifications),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
|
|
@ -742,7 +742,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
child: const Text('Skip for now'),
|
||||
child: Text(context.l10n.setupSkipForNow),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
|
@ -770,7 +770,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
_selectedDirectory != null ? 'Download Folder Selected!' : 'Choose Download Folder',
|
||||
_selectedDirectory != null ? context.l10n.setupFolderSelected : context.l10n.setupFolderChoose,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -802,7 +802,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Select a folder where your downloaded music will be saved.',
|
||||
context.l10n.setupFolderDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -814,7 +814,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
? SizedBox(width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.onPrimary))
|
||||
: Icon(_selectedDirectory != null ? Icons.edit_rounded : Icons.folder_open_rounded),
|
||||
label: Text(_selectedDirectory != null ? 'Change Folder' : 'Select Folder'),
|
||||
label: Text(_selectedDirectory != null ? context.l10n.setupChangeFolder : context.l10n.setupSelectFolder),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
|
|
@ -845,7 +845,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Spotify API (Optional)',
|
||||
context.l10n.setupSpotifyApiOptional,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -853,7 +853,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Add your Spotify API credentials for better search results, or skip to use Deezer instead.',
|
||||
context.l10n.setupSpotifyApiDescription,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -868,9 +868,9 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
clipBehavior: Clip.antiAlias,
|
||||
child: SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
title: Text('Use Spotify API', style: Theme.of(context).textTheme.titleSmall),
|
||||
title: Text(context.l10n.setupUseSpotifyApi, style: Theme.of(context).textTheme.titleSmall),
|
||||
subtitle: Text(
|
||||
_useSpotifyApi ? 'Enter your credentials below' : 'Using Deezer (no account needed)',
|
||||
_useSpotifyApi ? context.l10n.setupEnterCredentialsBelow : context.l10n.setupUsingDeezer,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
secondary: Container(
|
||||
|
|
@ -907,12 +907,12 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Client ID
|
||||
Text('Client ID', style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
Text(context.l10n.credentialsClientId, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _clientIdController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter Spotify Client ID',
|
||||
hintText: context.l10n.setupEnterClientId,
|
||||
prefixIcon: const Icon(Icons.key_rounded),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
|
@ -926,13 +926,13 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
const SizedBox(height: 16),
|
||||
|
||||
// Client Secret
|
||||
Text('Client Secret', style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
Text(context.l10n.credentialsClientSecret, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _clientSecretController,
|
||||
obscureText: !_showClientSecret,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter Spotify Client Secret',
|
||||
hintText: context.l10n.setupEnterClientSecret,
|
||||
prefixIcon: const Icon(Icons.lock_rounded),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_showClientSecret ? Icons.visibility_off_rounded : Icons.visibility_rounded),
|
||||
|
|
@ -962,7 +962,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Get credentials from developer.spotify.com',
|
||||
context.l10n.setupGetCredentialsFromSpotify,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onTertiaryContainer),
|
||||
),
|
||||
),
|
||||
|
|
@ -995,7 +995,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
TextButton.icon(
|
||||
onPressed: () => setState(() => _currentStep--),
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
label: const Text('Back'),
|
||||
label: Text(context.l10n.setupBack),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
|
|
@ -1011,9 +1011,9 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [Text('Next'), SizedBox(width: 8), Icon(Icons.arrow_forward_rounded, size: 18)],
|
||||
children: [Text(context.l10n.setupNext), const SizedBox(width: 8), const Icon(Icons.arrow_forward_rounded, size: 18)],
|
||||
),
|
||||
)
|
||||
else
|
||||
|
|
@ -1029,7 +1029,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
|||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_useSpotifyApi ? 'Get Started' : 'Skip & Start'),
|
||||
Text(_useSpotifyApi ? context.l10n.setupGetStarted : context.l10n.setupSkipAndStart),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.check_rounded, size: 18),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/store_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ class _ExtensionDetailsScreenState
|
|||
_buildInfoCard(context, liveExtension, colorScheme, isDownloading),
|
||||
_buildSectionHeader(
|
||||
context,
|
||||
'About',
|
||||
context.l10n.aboutTitle,
|
||||
Icons.info_outline,
|
||||
colorScheme,
|
||||
),
|
||||
|
|
@ -61,7 +62,7 @@ class _ExtensionDetailsScreenState
|
|||
|
||||
_buildSectionHeader(
|
||||
context,
|
||||
'Capabilities',
|
||||
context.l10n.extensionCapabilities,
|
||||
Icons.extension_outlined,
|
||||
colorScheme,
|
||||
),
|
||||
|
|
@ -173,9 +174,9 @@ class _ExtensionDetailsScreenState
|
|||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'by ${ext.author}',
|
||||
context.l10n.extensionsAuthor(ext.author),
|
||||
style: Theme.of(context).textTheme.bodyLarge
|
||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
|
|
@ -204,7 +205,7 @@ class _ExtensionDetailsScreenState
|
|||
),
|
||||
if (ext.isInstalled)
|
||||
_Badge(
|
||||
label: 'Installed',
|
||||
label: context.l10n.storeInstalled,
|
||||
color: colorScheme.primaryContainer,
|
||||
textColor: colorScheme.onPrimaryContainer,
|
||||
icon: Icons.check,
|
||||
|
|
@ -226,7 +227,7 @@ class _ExtensionDetailsScreenState
|
|||
FilledButton.icon(
|
||||
onPressed: () => _updateExtension(ext),
|
||||
icon: const Icon(Icons.update),
|
||||
label: Text('Update to v${ext.version}'),
|
||||
label: Text('${context.l10n.storeUpdate} v${ext.version}'),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -241,7 +242,7 @@ class _ExtensionDetailsScreenState
|
|||
child: OutlinedButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Installed'),
|
||||
label: Text(context.l10n.storeInstalled),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(0, 52),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -262,7 +263,7 @@ class _ExtensionDetailsScreenState
|
|||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
tooltip: 'Uninstall',
|
||||
tooltip: context.l10n.extensionsUninstall,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -270,7 +271,7 @@ class _ExtensionDetailsScreenState
|
|||
FilledButton.icon(
|
||||
onPressed: () => _installExtension(ext),
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('Install Extension'),
|
||||
label: Text(context.l10n.storeInstall),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -380,19 +381,19 @@ class _ExtensionDetailsScreenState
|
|||
child: Column(
|
||||
children: [
|
||||
_MetadataRow(
|
||||
label: 'Updated',
|
||||
label: context.l10n.extensionUpdated,
|
||||
value: ext.updatedAt.isNotEmpty
|
||||
? _formatDate(ext.updatedAt)
|
||||
? _formatDate(context, ext.updatedAt)
|
||||
: '-',
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
_MetadataRow(
|
||||
label: 'ID',
|
||||
label: context.l10n.extensionId,
|
||||
value: ext.id,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
_MetadataRow(
|
||||
label: 'Min App Version',
|
||||
label: context.l10n.extensionMinAppVersion,
|
||||
value: ext.minAppVersion ?? 'Any',
|
||||
colorScheme: colorScheme,
|
||||
isLast: true,
|
||||
|
|
@ -428,19 +429,19 @@ class _ExtensionDetailsScreenState
|
|||
children: [
|
||||
_CapabilityRow(
|
||||
icon: Icons.search,
|
||||
label: 'Metadata Provider',
|
||||
label: context.l10n.extensionMetadataProvider,
|
||||
enabled: isMetadataProvider,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
_CapabilityRow(
|
||||
icon: Icons.download,
|
||||
label: 'Download Provider',
|
||||
label: context.l10n.extensionDownloadProvider,
|
||||
enabled: isDownloadProvider,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
_CapabilityRow(
|
||||
icon: Icons.lyrics,
|
||||
label: 'Lyrics Provider',
|
||||
label: context.l10n.extensionLyricsProvider,
|
||||
enabled: isLyricsProvider,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
|
|
@ -458,22 +459,22 @@ class _ExtensionDetailsScreenState
|
|||
);
|
||||
}
|
||||
|
||||
String _formatDate(String dateStr) {
|
||||
String _formatDate(BuildContext context, String dateStr) {
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(date);
|
||||
|
||||
if (diff.inDays == 0) {
|
||||
return 'Today';
|
||||
return context.l10n.dateToday;
|
||||
} else if (diff.inDays == 1) {
|
||||
return 'Yesterday';
|
||||
return context.l10n.dateYesterday;
|
||||
} else if (diff.inDays < 7) {
|
||||
return '${diff.inDays} days ago';
|
||||
return context.l10n.dateDaysAgo(diff.inDays);
|
||||
} else if (diff.inDays < 30) {
|
||||
return '${(diff.inDays / 7).floor()} weeks ago';
|
||||
return context.l10n.dateWeeksAgo((diff.inDays / 7).floor());
|
||||
} else if (diff.inDays < 365) {
|
||||
return '${(diff.inDays / 30).floor()} months ago';
|
||||
return context.l10n.dateMonthsAgo((diff.inDays / 30).floor());
|
||||
} else {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
|
@ -530,8 +531,8 @@ class _ExtensionDetailsScreenState
|
|||
SnackBar(
|
||||
content: Text(
|
||||
success
|
||||
? '${ext.displayName} installed.'
|
||||
: 'Failed to install ${ext.displayName}',
|
||||
? context.l10n.snackbarExtensionInstalled(ext.displayName)
|
||||
: context.l10n.snackbarFailedToInstall,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
|
|
@ -551,8 +552,8 @@ class _ExtensionDetailsScreenState
|
|||
SnackBar(
|
||||
content: Text(
|
||||
success
|
||||
? '${ext.displayName} updated.'
|
||||
: 'Failed to update ${ext.displayName}',
|
||||
? context.l10n.snackbarExtensionUpdated(ext.displayName)
|
||||
: context.l10n.snackbarFailedToUpdate,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
|
|
@ -564,17 +565,17 @@ class _ExtensionDetailsScreenState
|
|||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Uninstall Extension?'),
|
||||
content: Text('Are you sure you want to remove ${ext.displayName}?'),
|
||||
title: Text(context.l10n.dialogUninstallExtension),
|
||||
content: Text(context.l10n.dialogUninstallExtensionMessage(ext.displayName)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(
|
||||
'Uninstall',
|
||||
context.l10n.dialogUninstall,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/store_provider.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/screens/store/extension_details_screen.dart';
|
||||
|
|
@ -74,7 +75,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
expandedTitleScale: 1.0,
|
||||
titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
title: Text(
|
||||
'Store',
|
||||
context.l10n.storeTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (14 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -93,7 +94,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search extensions...',
|
||||
hintText: context.l10n.storeSearch,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
|
|
@ -141,7 +142,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
child: Row(
|
||||
children: [
|
||||
_CategoryChip(
|
||||
label: 'All',
|
||||
label: context.l10n.storeFilterAll,
|
||||
icon: Icons.apps,
|
||||
isSelected: state.selectedCategory == null,
|
||||
onTap: () =>
|
||||
|
|
@ -149,7 +150,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_CategoryChip(
|
||||
label: 'Metadata',
|
||||
label: context.l10n.storeFilterMetadata,
|
||||
icon: Icons.label_outline,
|
||||
isSelected:
|
||||
state.selectedCategory == StoreCategory.metadata,
|
||||
|
|
@ -159,7 +160,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_CategoryChip(
|
||||
label: 'Download',
|
||||
label: context.l10n.storeFilterDownload,
|
||||
icon: Icons.download_outlined,
|
||||
isSelected:
|
||||
state.selectedCategory == StoreCategory.download,
|
||||
|
|
@ -169,7 +170,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_CategoryChip(
|
||||
label: 'Utility',
|
||||
label: context.l10n.storeFilterUtility,
|
||||
icon: Icons.build_outlined,
|
||||
isSelected:
|
||||
state.selectedCategory == StoreCategory.utility,
|
||||
|
|
@ -179,7 +180,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_CategoryChip(
|
||||
label: 'Lyrics',
|
||||
label: context.l10n.storeFilterLyrics,
|
||||
icon: Icons.lyrics_outlined,
|
||||
isSelected:
|
||||
state.selectedCategory == StoreCategory.lyrics,
|
||||
|
|
@ -189,7 +190,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
_CategoryChip(
|
||||
label: 'Integration',
|
||||
label: context.l10n.storeFilterIntegration,
|
||||
icon: Icons.link,
|
||||
isSelected:
|
||||
state.selectedCategory == StoreCategory.integration,
|
||||
|
|
@ -286,7 +287,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
onPressed: () =>
|
||||
ref.read(storeProvider.notifier).refresh(forceRefresh: true),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
label: Text(context.l10n.dialogRetry),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -321,7 +322,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
|||
_searchController.clear();
|
||||
ref.read(storeProvider.notifier).clearSearch();
|
||||
},
|
||||
child: const Text('Clear filters'),
|
||||
child: Text(context.l10n.storeClearFilters),
|
||||
),
|
||||
],
|
||||
],
|
||||
|
|
@ -574,7 +575,7 @@ class _ExtensionItem extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
minimumSize: const Size(0, 36),
|
||||
),
|
||||
child: const Text('Update'),
|
||||
child: Text(context.l10n.storeUpdate),
|
||||
)
|
||||
else if (extension.isInstalled)
|
||||
OutlinedButton(
|
||||
|
|
@ -602,7 +603,7 @@ class _ExtensionItem extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
minimumSize: const Size(0, 36),
|
||||
),
|
||||
child: const Text('Install'),
|
||||
child: Text(context.l10n.storeInstall),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
|
||||
/// Screen to display detailed metadata for a downloaded track
|
||||
/// Designed with Material Expressive 3 style
|
||||
|
|
@ -325,7 +326,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'File not found',
|
||||
context.l10n.trackFileNotFound,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontSize: 12,
|
||||
|
|
@ -361,7 +362,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Metadata',
|
||||
context.l10n.trackMetadata,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
|
|
@ -383,7 +384,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
return OutlinedButton.icon(
|
||||
onPressed: () => _openServiceUrl(context),
|
||||
icon: const Icon(Icons.open_in_new, size: 18),
|
||||
label: Text(isDeezer ? 'Open in Deezer' : 'Open in Spotify'),
|
||||
label: Text(isDeezer ? context.l10n.trackOpenInDeezer : context.l10n.trackOpenInSpotify),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -440,7 +441,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
if (context.mounted) {
|
||||
_copyToClipboard(context, webUrl);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${isDeezer ? 'Deezer' : 'Spotify'} URL copied to clipboard')),
|
||||
SnackBar(content: Text(context.l10n.snackbarUrlCopied(isDeezer ? 'Deezer' : 'Spotify'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -456,21 +457,21 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
}
|
||||
|
||||
final items = <_MetadataItem>[
|
||||
_MetadataItem('Track name', trackName),
|
||||
_MetadataItem('Artist', artistName),
|
||||
_MetadataItem(context.l10n.trackTrackName, trackName),
|
||||
_MetadataItem(context.l10n.trackArtist, artistName),
|
||||
if (albumArtist != null && albumArtist != artistName)
|
||||
_MetadataItem('Album artist', albumArtist!),
|
||||
_MetadataItem('Album', albumName),
|
||||
_MetadataItem(context.l10n.trackAlbumArtist, albumArtist!),
|
||||
_MetadataItem(context.l10n.trackAlbum, albumName),
|
||||
if (trackNumber != null && trackNumber! > 0)
|
||||
_MetadataItem('Track number', trackNumber.toString()),
|
||||
_MetadataItem(context.l10n.trackTrackNumber, trackNumber.toString()),
|
||||
if (discNumber != null && discNumber! > 0)
|
||||
_MetadataItem('Disc number', discNumber.toString()),
|
||||
_MetadataItem(context.l10n.trackDiscNumber, discNumber.toString()),
|
||||
if (item.duration != null)
|
||||
_MetadataItem('Duration', _formatDuration(item.duration!)),
|
||||
_MetadataItem(context.l10n.trackDuration, _formatDuration(item.duration!)),
|
||||
if (audioQualityStr != null)
|
||||
_MetadataItem('Audio quality', audioQualityStr),
|
||||
_MetadataItem(context.l10n.trackAudioQuality, audioQualityStr),
|
||||
if (releaseDate != null && releaseDate!.isNotEmpty)
|
||||
_MetadataItem('Release date', releaseDate!),
|
||||
_MetadataItem(context.l10n.trackReleaseDate, releaseDate!),
|
||||
if (isrc != null && isrc!.isNotEmpty)
|
||||
_MetadataItem('ISRC', isrc!),
|
||||
];
|
||||
|
|
@ -482,8 +483,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
}
|
||||
|
||||
items.addAll([
|
||||
_MetadataItem('Service', item.service.toUpperCase()),
|
||||
_MetadataItem('Downloaded', _formatFullDate(item.downloadedAt)),
|
||||
_MetadataItem(context.l10n.trackMetadataService, item.service.toUpperCase()),
|
||||
_MetadataItem(context.l10n.trackDownloaded, _formatFullDate(item.downloadedAt)),
|
||||
]);
|
||||
|
||||
return Column(
|
||||
|
|
@ -557,7 +558,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'File Info',
|
||||
context.l10n.trackFileInfo,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
|
|
@ -708,7 +709,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Lyrics',
|
||||
context.l10n.trackLyrics,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
|
|
@ -719,7 +720,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.copy, size: 20),
|
||||
onPressed: () => _copyToClipboard(context, _lyrics!),
|
||||
tooltip: 'Copy lyrics',
|
||||
tooltip: context.l10n.trackCopyLyrics,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -751,7 +752,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
),
|
||||
TextButton(
|
||||
onPressed: _fetchLyrics,
|
||||
child: const Text('Retry'),
|
||||
child: Text(context.l10n.dialogRetry),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -774,7 +775,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
child: FilledButton.tonalIcon(
|
||||
onPressed: _fetchLyrics,
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('Load Lyrics'),
|
||||
label: Text(context.l10n.trackLoadLyrics),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -806,7 +807,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
if (mounted) {
|
||||
if (result.isEmpty) {
|
||||
setState(() {
|
||||
_lyricsError = 'Lyrics not available for this track';
|
||||
_lyricsError = context.l10n.trackLyricsNotAvailable;
|
||||
_lyricsLoading = false;
|
||||
});
|
||||
} else {
|
||||
|
|
@ -821,8 +822,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
} catch (e) {
|
||||
if (mounted) {
|
||||
final errorMsg = e.toString().contains('TimeoutException')
|
||||
? 'Request timed out. Try again later.'
|
||||
: 'Failed to load lyrics';
|
||||
? context.l10n.trackLyricsTimeout
|
||||
: context.l10n.trackLyricsLoadFailed;
|
||||
setState(() {
|
||||
_lyricsError = errorMsg;
|
||||
_lyricsLoading = false;
|
||||
|
|
@ -856,7 +857,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
child: FilledButton.icon(
|
||||
onPressed: fileExists ? () => _openFile(context, cleanFilePath) : null,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: const Text('Play'),
|
||||
label: Text(context.l10n.trackMetadataPlay),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -872,7 +873,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
child: OutlinedButton.icon(
|
||||
onPressed: () => _confirmDelete(context, ref, colorScheme),
|
||||
icon: Icon(Icons.delete_outline, color: colorScheme.error),
|
||||
label: Text('Delete', style: TextStyle(color: colorScheme.error)),
|
||||
label: Text(context.l10n.trackMetadataDelete, style: TextStyle(color: colorScheme.error)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
@ -908,7 +909,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: const Text('Copy file path'),
|
||||
title: Text(context.l10n.trackCopyFilePath),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_copyToClipboard(context, cleanFilePath);
|
||||
|
|
@ -916,7 +917,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Share'),
|
||||
title: Text(context.l10n.trackMetadataShare),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_shareFile(context);
|
||||
|
|
@ -924,7 +925,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.delete, color: colorScheme.error),
|
||||
title: Text('Remove from device', style: TextStyle(color: colorScheme.error)),
|
||||
title: Text(context.l10n.trackRemoveFromDevice, style: TextStyle(color: colorScheme.error)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_confirmDelete(context, ref, colorScheme);
|
||||
|
|
@ -941,14 +942,12 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Remove from device?'),
|
||||
content: const Text(
|
||||
'This will permanently delete the downloaded file and remove it from your history.',
|
||||
),
|
||||
title: Text(context.l10n.trackDeleteConfirmTitle),
|
||||
content: Text(context.l10n.trackDeleteConfirmMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
|
|
@ -970,7 +969,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
Navigator.pop(context); // Go back to history
|
||||
}
|
||||
},
|
||||
child: Text('Delete', style: TextStyle(color: colorScheme.error)),
|
||||
child: Text(context.l10n.dialogDelete, style: TextStyle(color: colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -983,13 +982,13 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
final result = await OpenFilex.open(filePath, type: mimeType);
|
||||
if (result.type != ResultType.done && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Cannot open: ${result.message}')),
|
||||
SnackBar(content: Text(context.l10n.trackCannotOpen(result.message))),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Cannot open file: $e')),
|
||||
SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -998,9 +997,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
void _copyToClipboard(BuildContext context, String text) {
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Copied to clipboard'),
|
||||
duration: Duration(seconds: 2),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.trackCopiedToClipboard),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -1010,7 +1009,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
|||
if (!await file.exists()) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('File not found')),
|
||||
SnackBar(content: Text(context.l10n.snackbarFileNotFound)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
|
||||
/// Built-in service info with quality options
|
||||
class BuiltInService {
|
||||
|
|
@ -167,7 +168,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
|
||||
child: Text(
|
||||
'Download From',
|
||||
context.l10n.downloadFrom,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
|
@ -202,7 +203,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
|
||||
child: Text(
|
||||
'Select Quality',
|
||||
context.l10n.downloadSelectQuality,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
|
@ -212,7 +213,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
|
||||
child: Text(
|
||||
'Actual quality depends on track availability. Hi-Res may not be available for all tracks.',
|
||||
context.l10n.qualityNote,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:spotiflac_android/constants/app_info.dart';
|
|||
import 'package:spotiflac_android/services/update_checker.dart';
|
||||
import 'package:spotiflac_android/services/apk_downloader.dart';
|
||||
import 'package:spotiflac_android/services/notification_service.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
|
||||
class UpdateDialog extends StatefulWidget {
|
||||
final UpdateInfo updateInfo;
|
||||
|
|
@ -42,7 +43,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
setState(() {
|
||||
_isDownloading = true;
|
||||
_progress = 0;
|
||||
_statusText = 'Starting download...';
|
||||
_statusText = context.l10n.updateStartingDownload;
|
||||
});
|
||||
|
||||
final notificationService = NotificationService();
|
||||
|
|
@ -91,11 +92,11 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
if (mounted) {
|
||||
setState(() {
|
||||
_isDownloading = false;
|
||||
_statusText = 'Download failed';
|
||||
_statusText = context.l10n.updateDownloadFailed;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to download update')),
|
||||
SnackBar(content: Text(context.l10n.updateFailedMessage)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,9 +132,9 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Update Available', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(context.l10n.updateAvailable, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 2),
|
||||
Text('A new version is ready', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
Text(context.l10n.updateNewVersionReady, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -154,11 +155,11 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_VersionChip(version: AppInfo.version, label: 'Current', colorScheme: colorScheme),
|
||||
_VersionChip(version: AppInfo.version, label: context.l10n.updateCurrent, colorScheme: colorScheme),
|
||||
const SizedBox(width: 12),
|
||||
Icon(Icons.arrow_forward_rounded, size: 20, color: colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
_VersionChip(version: widget.updateInfo.version, label: 'New', colorScheme: colorScheme, isNew: true),
|
||||
_VersionChip(version: widget.updateInfo.version, label: context.l10n.updateNew, colorScheme: colorScheme, isNew: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -184,7 +185,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Downloading...', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
||||
Text(context.l10n.updateDownloading, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -209,7 +210,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
),
|
||||
] else ...[
|
||||
// Changelog section
|
||||
Text("What's New", style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(context.l10n.updateWhatsNew, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 180),
|
||||
|
|
@ -240,7 +241,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.dialogCancel),
|
||||
),
|
||||
)
|
||||
else
|
||||
|
|
@ -251,7 +252,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
child: FilledButton.icon(
|
||||
onPressed: _downloadAndInstall,
|
||||
icon: const Icon(Icons.download_rounded, size: 20),
|
||||
label: const Text('Download & Install'),
|
||||
label: Text(context.l10n.updateDownloadInstall),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
|
|
@ -271,7 +272,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text("Don't remind", style: TextStyle(color: colorScheme.onSurfaceVariant)),
|
||||
child: Text(context.l10n.updateDontRemind, style: TextStyle(color: colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -285,7 +286,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
|||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text('Later'),
|
||||
child: Text(context.l10n.updateLater),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
13
pubspec.lock
13
pubspec.lock
|
|
@ -382,6 +382,11 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -488,6 +493,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# Localization
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: any
|
||||
|
||||
# State Management
|
||||
flutter_riverpod: ^3.1.0
|
||||
riverpod_annotation: ^4.0.0
|
||||
|
|
@ -77,6 +82,7 @@ flutter_launcher_icons:
|
|||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
generate: true
|
||||
|
||||
assets:
|
||||
- assets/images/
|
||||
|
|
|
|||
Loading…
Reference in a new issue