feat(i18n): replace all hardcoded strings with l10n keys across 13 screens

- Added 80+ new keys to app_en.arb covering lyrics, SAF, download settings,
  snackbars, dialogs, home, cache, and store screens
- Replaced hardcoded strings in main_shell, album_screen, playlist_screen,
  library_tracks_folder_screen, home_tab, settings_tab, download_settings_page,
  lyrics_provider_priority_page, track_metadata_screen, extension_detail_page,
  cache_management_page, local_album_screen, downloaded_album_screen, search_screen
- Fixed structural bug in track_metadata_screen (duplicate closing brace)
- Added missing l10n.dart import to search_screen.dart
- Regenerated all app_localizations*.dart files via flutter gen-l10n
This commit is contained in:
zarzet committed 2026-03-13 15:12:12 +07:00
1 parent 16669d8b7a
commit 48f614359e
29 files changed
+5831 -161

No files matched your search

+12 -4
View File
@@ -619,7 +619,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
size: 22,
color: allLoved ? Colors.redAccent : Colors.white,
),
tooltip: allLoved ? 'Remove from Loved' : 'Love All',
tooltip: allLoved ? context.l10n.trackOptionRemoveFromLoved : context.l10n.tooltipLoveAll,
padding: EdgeInsets.zero,
),
);
@@ -642,7 +642,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
? null
: () => showAddTracksToPlaylistSheet(context, ref, _tracks!),
icon: const Icon(Icons.add, size: 22, color: Colors.white),
tooltip: 'Add to Playlist',
tooltip: context.l10n.tooltipAddToPlaylist,
padding: EdgeInsets.zero,
),
);
@@ -660,7 +660,11 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Removed ${tracks.length} tracks from Loved')),
SnackBar(
content: Text(
context.l10n.snackbarRemovedTracksFromLoved(tracks.length),
),
),
);
}
} else {
@@ -673,7 +677,11 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Added $addedCount tracks to Loved')),
SnackBar(
content: Text(
context.l10n.snackbarAddedTracksToLoved(addedCount),
),
),
);
}
}
+1 -1
View File
@@ -363,7 +363,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
if (tracks.isEmpty) {
return Scaffold(
appBar: AppBar(title: Text(widget.albumName)),
body: Center(child: Text('No tracks found for this album')),
body: Center(child: Text(context.l10n.noTracksFoundForAlbum)),
);
}
+3 -3
View File
@@ -816,7 +816,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
const SizedBox(height: 12),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Skip already downloaded songs'),
title: Text(l10n.homeSkipAlreadyDownloaded),
value: skipDownloaded,
onChanged: (value) {
setDialogState(() {
@@ -1750,7 +1750,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
),
ListTile(
leading: Icon(Icons.album, color: colorScheme.onSurfaceVariant),
title: const Text('Go to Album'),
title: Text(context.l10n.homeGoToAlbum),
onTap: () {
Navigator.pop(context);
_navigateToTrackAlbum(item);
@@ -1824,7 +1824,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Album info not available')));
).showSnackBar(SnackBar(content: Text(context.l10n.homeAlbumInfoUnavailable)));
}
}
@@ -850,8 +850,8 @@ class _LibraryTracksFolderScreenState
final colorScheme = Theme.of(dialogContext).colorScheme;
return AlertDialog(
backgroundColor: colorScheme.surfaceContainerHigh,
title: const Text('Download All'),
content: Text('Download ${tracks.length} tracks?'),
title: Text(context.l10n.dialogDownloadAllTitle),
content: Text(context.l10n.dialogDownloadAllMessage(tracks.length)),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
@@ -862,7 +862,7 @@ class _LibraryTracksFolderScreenState
Navigator.pop(dialogContext);
_downloadAll(tracks);
},
child: const Text('Download'),
child: Text(context.l10n.dialogDownload),
),
],
);
+1 -1
View File
@@ -247,7 +247,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
if (tracks.isEmpty) {
return Scaffold(
appBar: AppBar(title: Text(widget.albumName)),
body: const Center(child: Text('No tracks found for this album')),
body: Center(child: Text(context.l10n.noTracksFoundForAlbum)),
);
}
+9 -14
View File
@@ -181,25 +181,20 @@ class _MainShellState extends ConsumerState<MainShell> {
size: 32,
color: colorScheme.primary,
),
title: const Text('Storage Update Required'),
content: const Column(
title: Text(context.l10n.safMigrationTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. '
'This fixes "permission denied" errors on Android 10+.',
),
SizedBox(height: 12),
Text(
'Please select your download folder again to switch to the new storage system.',
),
Text(context.l10n.safMigrationMessage1),
const SizedBox(height: 12),
Text(context.l10n.safMigrationMessage2),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Later'),
child: Text(context.l10n.updateLater),
),
FilledButton(
onPressed: () async {
@@ -219,15 +214,15 @@ class _MainShellState extends ConsumerState<MainShell> {
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Download folder updated to SAF mode'),
SnackBar(
content: Text(context.l10n.safMigrationSuccess),
),
);
}
}
}
},
child: const Text('Select Folder'),
child: Text(context.l10n.setupSelectFolder),
),
],
),
+15 -7
View File
@@ -482,7 +482,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
size: 22,
color: allLoved ? Colors.redAccent : Colors.white,
),
tooltip: allLoved ? 'Remove from Loved' : 'Love All',
tooltip: allLoved ? context.l10n.trackOptionRemoveFromLoved : context.l10n.tooltipLoveAll,
padding: EdgeInsets.zero,
),
);
@@ -505,7 +505,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
Widget _buildAddToPlaylistButton(BuildContext context) {
return _buildCircleButton(
icon: Icons.playlist_add,
tooltip: 'Add to Playlist',
tooltip: context.l10n.tooltipAddToPlaylist,
onPressed: _tracks.isEmpty
? null
: () => showAddTracksToPlaylistSheet(context, ref, _tracks),
@@ -520,8 +520,8 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
final colorScheme = Theme.of(dialogContext).colorScheme;
return AlertDialog(
backgroundColor: colorScheme.surfaceContainerHigh,
title: const Text('Download All'),
content: Text('Download ${_tracks.length} tracks?'),
title: Text(context.l10n.dialogDownloadAllTitle),
content: Text(context.l10n.dialogDownloadAllMessage(_tracks.length)),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
@@ -532,7 +532,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
Navigator.pop(dialogContext);
_downloadAll(context);
},
child: const Text('Download'),
child: Text(context.l10n.dialogDownload),
),
],
);
@@ -552,7 +552,11 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Removed ${tracks.length} tracks from Loved')),
SnackBar(
content: Text(
context.l10n.snackbarRemovedTracksFromLoved(tracks.length),
),
),
);
}
} else {
@@ -565,7 +569,11 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Added $addedCount tracks to Loved')),
SnackBar(
content: Text(
context.l10n.snackbarAddedTracksToLoved(addedCount),
),
),
);
}
}
+2 -1
View File
@@ -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/services/cover_cache_manager.dart';
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/providers/track_provider.dart';
@@ -52,7 +53,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
.addToQueue(track, settings.defaultService);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Added "${track.name}" to queue')));
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarAddedToQueue(track.name))));
}
@override
@@ -56,7 +56,7 @@ class _CacheManagementPageState extends ConsumerState<CacheManagementPage> {
setState(() => _isLoading = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString()))));
}
}
@@ -282,7 +282,7 @@ class _CacheManagementPageState extends ConsumerState<CacheManagementPage> {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString()))));
} finally {
if (mounted) {
setState(() => _busyAction = null);
@@ -394,7 +394,7 @@ class _CacheManagementPageState extends ConsumerState<CacheManagementPage> {
),
actions: [
IconButton(
tooltip: 'Refresh',
tooltip: context.l10n.cacheRefresh,
onPressed: _isBusy ? null : _refreshOverview,
icon: const Icon(Icons.refresh),
),
@@ -376,7 +376,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
title: context.l10n.downloadAskBeforeDownload,
subtitle: isBuiltInService
? context.l10n.downloadAskQualitySubtitle
: 'Select a built-in service to enable',
: context.l10n.downloadSelectServiceToEnable,
value: settings.askQualityBeforeDownload,
enabled: isBuiltInService,
onChanged: (value) => ref
@@ -413,7 +413,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
// Lossy 320kbps option (Tidal only) - downloads M4A, converts to MP3/Opus
if (isTidalService)
_QualityOption(
title: 'Lossy 320kbps',
title: context.l10n.downloadLossy320,
subtitle: _getTidalHighFormatLabel(
settings.tidalHighFormat,
),
@@ -426,7 +426,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
if (isTidalService && settings.audioQuality == 'HIGH')
SettingsItem(
icon: Icons.tune,
title: 'Lossy Format',
title: context.l10n.downloadLossyFormat,
subtitle: _getTidalHighFormatLabel(
settings.tidalHighFormat,
),
@@ -451,7 +451,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
const SizedBox(width: 8),
Expanded(
child: Text(
'Select Tidal or Qobuz above to configure quality',
context.l10n.downloadSelectTidalQobuz,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: colorScheme.onSurfaceVariant,
@@ -504,7 +504,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
title: context.l10n.optionsEmbedLyrics,
subtitle: settings.embedMetadata
? context.l10n.optionsEmbedLyricsSubtitle
: 'Disabled while Embed Metadata is turned off',
: context.l10n.downloadEmbedLyricsDisabled,
value: settings.embedLyrics,
enabled: settings.embedMetadata,
onChanged: (value) => ref
@@ -528,7 +528,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SettingsItem(
icon: Icons.source_outlined,
title: 'Lyrics Providers',
title: context.l10n.lyricsProvidersTitle,
subtitle: _getLyricsProvidersSubtitle(
settings.lyricsProviders,
),
@@ -541,10 +541,10 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SettingsSwitchItem(
icon: Icons.translate_outlined,
title: 'Netease: Include Translation',
title: context.l10n.downloadNeteaseIncludeTranslation,
subtitle: settings.lyricsIncludeTranslationNetease
? 'Append translated lyrics when available'
: 'Use original lyrics only',
? context.l10n.downloadNeteaseIncludeTranslationEnabled
: context.l10n.downloadNeteaseIncludeTranslationDisabled,
value: settings.lyricsIncludeTranslationNetease,
onChanged: (value) => ref
.read(settingsProvider.notifier)
@@ -552,10 +552,10 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SettingsSwitchItem(
icon: Icons.text_fields_outlined,
title: 'Netease: Include Romanization',
title: context.l10n.downloadNeteaseIncludeRomanization,
subtitle: settings.lyricsIncludeRomanizationNetease
? 'Append romanized lyrics when available'
: 'Disabled',
? context.l10n.downloadNeteaseIncludeRomanizationEnabled
: context.l10n.downloadNeteaseIncludeRomanizationDisabled,
value: settings.lyricsIncludeRomanizationNetease,
onChanged: (value) => ref
.read(settingsProvider.notifier)
@@ -563,10 +563,10 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SettingsSwitchItem(
icon: Icons.record_voice_over_outlined,
title: 'Apple/QQ Multi-Person Word-by-Word',
title: context.l10n.downloadAppleQqMultiPerson,
subtitle: settings.lyricsMultiPersonWordByWord
? 'Enable v1/v2 speaker and [bg:] tags'
: 'Simplified word-by-word formatting',
? context.l10n.downloadAppleQqMultiPersonEnabled
: context.l10n.downloadAppleQqMultiPersonDisabled,
value: settings.lyricsMultiPersonWordByWord,
onChanged: (value) => ref
.read(settingsProvider.notifier)
@@ -574,9 +574,9 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SettingsItem(
icon: Icons.language_outlined,
title: 'Musixmatch Language',
title: context.l10n.downloadMusixmatchLanguage,
subtitle: settings.musixmatchLanguage.isEmpty
? 'Auto (original)'
? context.l10n.downloadMusixmatchLanguageAuto
: settings.musixmatchLanguage.toUpperCase(),
onTap: () => _showMusixmatchLanguagePicker(
context,
@@ -622,8 +622,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
icon: Icons.library_music_outlined,
title: context.l10n.downloadSeparateSinglesFolder,
subtitle: settings.separateSingles
? 'Albums/ and Singles/ folders'
: 'All files in same structure',
? context.l10n.downloadSeparateSinglesEnabled
: context.l10n.downloadSeparateSinglesDisabled,
value: settings.separateSingles,
onChanged: (value) => ref
.read(settingsProvider.notifier)
@@ -670,9 +670,9 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
.read(settingsProvider.notifier)
.setUseAlbumArtistForFolders(value),
),
SettingsItem(
SettingsItem(
icon: Icons.filter_alt_outlined,
title: 'Artist Name Filters',
title: context.l10n.downloadArtistNameFilters,
subtitle: _getArtistFolderFilterSubtitle(
context,
usePrimaryArtistOnly: settings.usePrimaryArtistOnly,
@@ -707,28 +707,16 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
if (_artistFolderFiltersExpanded)
SettingsSwitchItem(
icon: Icons.group_remove_outlined,
title: 'Filter contributing artists in Album Artist',
title: context.l10n.downloadFilterContributing,
subtitle: settings.filterContributingArtistsInAlbumArtist
? 'Album Artist metadata uses primary artist only'
: 'Keep full Album Artist metadata value',
? context.l10n.downloadFilterContributingEnabled
: context.l10n.downloadFilterContributingDisabled,
value: settings.filterContributingArtistsInAlbumArtist,
onChanged: (value) => ref
.read(settingsProvider.notifier)
.setFilterContributingArtistsInAlbumArtist(value),
showDivider: false,
),
SettingsSwitchItem(
icon: Icons.person_outline,
title: context.l10n.downloadUsePrimaryArtistOnly,
subtitle: settings.usePrimaryArtistOnly
? context.l10n.downloadUsePrimaryArtistOnlyEnabled
: context.l10n.downloadUsePrimaryArtistOnlyDisabled,
value: settings.usePrimaryArtistOnly,
onChanged: (value) => ref
.read(settingsProvider.notifier)
.setUsePrimaryArtistOnly(value),
showDivider: false,
),
],
),
),
@@ -753,7 +741,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SettingsItem(
icon: Icons.public,
title: 'SongLink Region',
title: context.l10n.downloadSongLinkRegion,
subtitle: _getSongLinkRegionLabel(settings.songLinkRegion),
onTap: () => _showSongLinkRegionPicker(
context,
@@ -763,10 +751,10 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
SettingsSwitchItem(
icon: Icons.security_outlined,
title: 'Network compatibility mode',
title: context.l10n.downloadNetworkCompatibilityMode,
subtitle: settings.networkCompatibilityMode
? 'Enabled: try HTTP + accept invalid TLS certificates (unsafe)'
: 'Off: strict HTTPS certificate validation (recommended)',
? context.l10n.downloadNetworkCompatibilityModeEnabled
: context.l10n.downloadNetworkCompatibilityModeDisabled,
value: settings.networkCompatibilityMode,
onChanged: (value) {
ref
@@ -1045,7 +1033,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
const SizedBox(height: 8),
Text(
'Customize how your files are named.',
context.l10n.downloadFilenameDescription,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -1070,7 +1058,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
const SizedBox(height: 24),
Text(
'Tap to insert tag:',
context.l10n.downloadFilenameInsertTag,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -1238,7 +1226,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Text(
'Download Location',
context.l10n.setupDownloadLocationTitle,
style: Theme.of(
ctx,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
@@ -1247,7 +1235,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Text(
'Choose storage mode for downloaded files.',
context.l10n.downloadLocationSubtitle,
style: Theme.of(ctx).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -1255,8 +1243,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
ListTile(
leading: Icon(Icons.folder_special, color: colorScheme.primary),
title: const Text('App folder (non-SAF)'),
subtitle: const Text('Use default Music/SpotiFLAC path'),
title: Text(context.l10n.storageModeAppFolder),
subtitle: Text(context.l10n.storageModeAppFolderSubtitle),
trailing: !isSafMode ? const Icon(Icons.check) : null,
onTap: () async {
Navigator.pop(ctx);
@@ -1269,10 +1257,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
ListTile(
leading: Icon(Icons.folder_open, color: colorScheme.primary),
title: const Text('SAF folder'),
subtitle: const Text(
'Pick folder via Android Storage Access Framework',
),
title: Text(context.l10n.storageModeSaf),
subtitle: Text(context.l10n.storageModeSafSubtitle),
trailing: isSafMode ? const Icon(Icons.check) : null,
onTap: () async {
Navigator.pop(ctx);
@@ -1546,7 +1532,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
};
String _getLyricsProvidersSubtitle(List<String> providers) {
if (providers.isEmpty) return 'None enabled';
if (providers.isEmpty) return context.l10n.downloadProvidersNoneEnabled;
return providers.map((p) => _providerDisplayNames[p] ?? p).join(' > ');
}
@@ -1645,14 +1631,14 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Musixmatch Language',
context.l10n.downloadMusixmatchLanguage,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Set preferred language code (example: en, es, ja). Leave empty for auto.',
context.l10n.downloadMusixmatchLanguageDesc,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -1661,9 +1647,9 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
TextField(
controller: controller,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
labelText: 'Language code',
hintText: 'auto / en / es / ja',
decoration: InputDecoration(
labelText: context.l10n.downloadMusixmatchLanguageCode,
hintText: context.l10n.downloadMusixmatchLanguageHint,
),
),
const SizedBox(height: 16),
@@ -1682,7 +1668,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
.setMusixmatchLanguage('');
Navigator.pop(context);
},
child: const Text('Auto'),
child: Text(context.l10n.downloadMusixmatchAuto),
),
const SizedBox(width: 8),
FilledButton(
@@ -1739,7 +1725,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Text(
'Lossy 320kbps Format',
context.l10n.downloadLossy320Format,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
@@ -1748,7 +1734,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Text(
'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.',
context.l10n.downloadLossy320FormatDesc,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -1756,8 +1742,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
ListTile(
leading: const Icon(Icons.audiotrack),
title: const Text('MP3 320kbps'),
subtitle: const Text('Best compatibility, ~10MB per track'),
title: Text(context.l10n.downloadLossyMp3),
subtitle: Text(context.l10n.downloadLossyMp3Subtitle),
trailing: current == 'mp3_320'
? Icon(Icons.check, color: colorScheme.primary)
: null,
@@ -1770,8 +1756,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
ListTile(
leading: const Icon(Icons.graphic_eq),
title: const Text('Opus 256kbps'),
subtitle: const Text('Best quality Opus, ~8MB per track'),
title: Text(context.l10n.downloadLossyOpus256),
subtitle: Text(context.l10n.downloadLossyOpus256Subtitle),
trailing: current == 'opus_256'
? Icon(Icons.check, color: colorScheme.primary)
: null,
@@ -1784,8 +1770,8 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
ListTile(
leading: const Icon(Icons.graphic_eq),
title: const Text('Opus 128kbps'),
subtitle: const Text('Smallest size, ~4MB per track'),
title: Text(context.l10n.downloadLossyOpus128),
subtitle: Text(context.l10n.downloadLossyOpus128Subtitle),
trailing: current == 'opus_128'
? Icon(Icons.check, color: colorScheme.primary)
: null,
@@ -1842,7 +1828,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
ListTile(
leading: const Icon(Icons.signal_cellular_alt),
title: Text(context.l10n.settingsDownloadNetworkAny),
subtitle: const Text('WiFi + Mobile Data'),
subtitle: Text(context.l10n.downloadNetworkAnySubtitle),
trailing: current == 'any'
? Icon(Icons.check, color: colorScheme.primary)
: null,
@@ -1856,7 +1842,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
ListTile(
leading: const Icon(Icons.wifi),
title: Text(context.l10n.settingsDownloadNetworkWifiOnly),
subtitle: const Text('Pause downloads on mobile data'),
subtitle: Text(context.l10n.downloadNetworkWifiOnlySubtitle),
trailing: current == 'wifi_only'
? Icon(Icons.check, color: colorScheme.primary)
: null,
@@ -1897,17 +1883,17 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Text(
'SongLink Region',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
child: Text(
context.l10n.downloadSongLinkRegion,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Text(
'Used as userCountry for SongLink API lookup.',
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Text(
context.l10n.downloadSongLinkRegionDesc,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
@@ -1968,12 +1954,12 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Text(
'Folder Organization',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
child: Text(
context.l10n.downloadFolderOrganization,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
@@ -801,7 +801,7 @@ class _SettingItemState extends State<_SettingItem> {
Future<void> _invokeAction(BuildContext context) async {
if (widget.setting.action == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No action defined for this button')),
SnackBar(content: Text(context.l10n.snackbarNoActionDefined)),
);
return;
}
@@ -834,7 +834,7 @@ class _SettingItemState extends State<_SettingItem> {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString()))));
}
} finally {
if (mounted) {
@@ -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/widgets/priority_settings_scaffold.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
@@ -55,18 +56,18 @@ class _LyricsProviderPriorityPageState
return PrioritySettingsScaffold(
hasChanges: _hasChanges,
title: 'Lyrics Providers',
description:
'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.',
infoText:
'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.',
title: context.l10n.lyricsProvidersTitle,
description: context.l10n.lyricsProvidersDescription,
infoText: context.l10n.lyricsProvidersInfoText,
onSave: _saveChanges,
onConfirmDiscard: _confirmDiscard,
slivers: [
if (_enabledProviders.isNotEmpty)
SliverToBoxAdapter(
child: SettingsSectionHeader(
title: 'Enabled (${_enabledProviders.length})',
title: context.l10n.lyricsProvidersEnabledSection(
_enabledProviders.length,
),
),
),
if (_enabledProviders.isNotEmpty)
@@ -76,7 +77,7 @@ class _LyricsProviderPriorityPageState
itemCount: _enabledProviders.length,
itemBuilder: (context, index) {
final id = _enabledProviders[index];
final info = _getLyricsProviderInfo(id);
final info = _getLyricsProviderInfo(id, context);
return _EnabledProviderItem(
key: ValueKey(id),
providerId: id,
@@ -99,7 +100,9 @@ class _LyricsProviderPriorityPageState
if (disabled.isNotEmpty)
SliverToBoxAdapter(
child: SettingsSectionHeader(
title: 'Disabled (${disabled.length})',
title: context.l10n.lyricsProvidersDisabledSection(
disabled.length,
),
),
),
if (disabled.isNotEmpty)
@@ -108,7 +111,7 @@ class _LyricsProviderPriorityPageState
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final id = disabled[index];
final info = _getLyricsProviderInfo(id);
final info = _getLyricsProviderInfo(id, context);
return _DisabledProviderItem(
key: ValueKey(id),
providerId: id,
@@ -130,8 +133,8 @@ class _LyricsProviderPriorityPageState
void _disableProvider(String id) {
if (_enabledProviders.length <= 1) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('At least one provider must remain enabled'),
SnackBar(
content: Text(context.l10n.lyricsProvidersAtLeastOne),
),
);
return;
@@ -150,7 +153,7 @@ class _LyricsProviderPriorityPageState
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Lyrics provider priority saved')),
SnackBar(content: Text(context.l10n.lyricsProvidersSaved)),
);
}
}
@@ -159,16 +162,16 @@ class _LyricsProviderPriorityPageState
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Discard changes?'),
content: const Text('You have unsaved changes that will be lost.'),
title: Text(context.l10n.dialogDiscardChanges),
content: Text(context.l10n.lyricsProvidersDiscardContent),
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),
),
],
),
@@ -176,48 +179,51 @@ class _LyricsProviderPriorityPageState
return result ?? false;
}
static _LyricsProviderInfo _getLyricsProviderInfo(String id) {
static _LyricsProviderInfo _getLyricsProviderInfo(
String id,
BuildContext context,
) {
switch (id) {
case 'spotify_api':
return _LyricsProviderInfo(
name: 'Spotify Lyrics API',
description: 'Spotify-sourced synced lyrics via community API',
description: context.l10n.lyricsProviderSpotifyApiDesc,
icon: Icons.music_note_outlined,
);
case 'lrclib':
return _LyricsProviderInfo(
name: 'LRCLIB',
description: 'Open-source synced lyrics database',
description: context.l10n.lyricsProviderLrclibDesc,
icon: Icons.subtitles_outlined,
);
case 'netease':
return _LyricsProviderInfo(
name: 'Netease',
description: 'NetEase Cloud Music (good for Asian songs)',
description: context.l10n.lyricsProviderNeteaseDesc,
icon: Icons.cloud_outlined,
);
case 'musixmatch':
return _LyricsProviderInfo(
name: 'Musixmatch',
description: 'Largest lyrics database (multi-language)',
description: context.l10n.lyricsProviderMusixmatchDesc,
icon: Icons.translate,
);
case 'apple_music':
return _LyricsProviderInfo(
name: 'Apple Music',
description: 'Word-by-word synced lyrics (via proxy)',
description: context.l10n.lyricsProviderAppleMusicDesc,
icon: Icons.music_note,
);
case 'qqmusic':
return _LyricsProviderInfo(
name: 'QQ Music',
description: 'QQ Music (good for Chinese songs, via proxy)',
description: context.l10n.lyricsProviderQqMusicDesc,
icon: Icons.queue_music,
);
default:
return _LyricsProviderInfo(
name: id,
description: 'Extension provider',
description: context.l10n.lyricsProviderExtensionDesc,
icon: Icons.extension,
);
}
+2 -2
View File
@@ -107,8 +107,8 @@ class SettingsTab extends ConsumerWidget {
),
SettingsItem(
icon: Icons.favorite_outline,
title: 'Donate',
subtitle: 'Support SpotiFLAC-Mobile development',
title: l10n.settingsDonate,
subtitle: l10n.settingsDonateSubtitle,
onTap: () => _navigateTo(context, const DonatePage()),
showDivider: false,
),
+12 -12
View File
@@ -1731,13 +1731,13 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
);
success = ok;
if (!ok) {
error = 'Failed to write back to storage';
error = context.l10n.snackbarFailedToWriteStorage;
}
} else {
success = true;
}
} else {
error = result['error']?.toString() ?? 'Failed to embed lyrics';
error = result['error']?.toString() ?? context.l10n.snackbarFailedToEmbedLyrics;
}
} else if (isMp3 || isOpus) {
final metadata = _buildFallbackMetadata();
@@ -1783,7 +1783,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
if (ffmpegResult == null) {
error = 'Failed to embed lyrics';
error = context.l10n.snackbarFailedToEmbedLyrics;
} else if (_isSafFile) {
final ok = await PlatformBridge.writeTempToSaf(
ffmpegResult,
@@ -1791,13 +1791,13 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
);
success = ok;
if (!ok) {
error = 'Failed to write back to storage';
error = context.l10n.snackbarFailedToWriteStorage;
}
} else {
success = true;
}
} else {
error = 'Unsupported audio format';
error = context.l10n.snackbarUnsupportedAudioFormat;
}
if (mounted) {
@@ -1812,7 +1812,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
} else {
setState(() => _isEmbedding = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error ?? 'Failed to embed lyrics')),
SnackBar(content: Text(error ?? context.l10n.snackbarFailedToEmbedLyrics)),
);
}
}
@@ -1821,7 +1821,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
setState(() => _isEmbedding = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString()))));
}
} finally {
if (coverPath != null) {
@@ -2271,7 +2271,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
SnackBar(
content: Text(
context.l10n.trackSaveFailed(
'Failed to write back to storage',
context.l10n.snackbarFailedToWriteStorage,
),
),
),
@@ -2873,7 +2873,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
// Show loading indicator
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Loading CUE sheet...')),
SnackBar(content: Text(context.l10n.snackbarLoadingCueSheet)),
);
try {
@@ -3644,7 +3644,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
if (saved == true && mounted) {
ScaffoldMessenger.of(this.context).showSnackBar(
const SnackBar(content: Text('Metadata saved successfully')),
SnackBar(content: Text(this.context.l10n.snackbarMetadataSaved)),
);
// Re-read metadata from file to refresh the display
try {
@@ -4066,7 +4066,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to pick cover: $e')));
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString()))));
}
}
@@ -4290,7 +4290,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to save metadata: $e')));
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString()))));
}
} finally {
if (mounted) setState(() => _saving = false);