mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 11:05:38 +02:00
l10n: localize library settings and announcements
Move hardcoded strings in library settings, announcement link errors, and unknown track title/artist fallbacks to AppLocalizations. Sync locale-dependent fallback strings from MainShell.
This commit is contained in:
@@ -19,6 +19,7 @@ import 'package:spotiflac_android/screens/settings/settings_tab.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/services/shell_navigation_service.dart';
|
||||
import 'package:spotiflac_android/services/share_intent_service.dart';
|
||||
import 'package:spotiflac_android/services/music_player_service.dart';
|
||||
import 'package:spotiflac_android/services/notification_service.dart';
|
||||
import 'package:spotiflac_android/services/app_remote_config_service.dart';
|
||||
import 'package:spotiflac_android/services/update_checker.dart';
|
||||
@@ -61,7 +62,12 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
NotificationService().updateStrings(context.l10n);
|
||||
final l10n = context.l10n;
|
||||
NotificationService().updateStrings(l10n);
|
||||
updateMusicPlayerStrings(
|
||||
unknownTitle: l10n.unknownTitle,
|
||||
unknownArtist: l10n.unknownArtist,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show ScrollDirection;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
@@ -147,7 +148,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
),
|
||||
body: const Center(child: Text('Nothing is playing')),
|
||||
body: Center(child: Text(context.l10n.nowPlayingNothingPlaying)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,16 +159,16 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
appBar: AppBar(
|
||||
backgroundColor: colorScheme.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
title: const Text('Now Playing'),
|
||||
title: Text(context.l10n.nowPlayingTitle),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
tooltip: 'Minimize',
|
||||
tooltip: context.l10n.nowPlayingMinimize,
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Up next',
|
||||
tooltip: context.l10n.nowPlayingUpNext,
|
||||
icon: const Icon(Icons.queue_music),
|
||||
onPressed: () => _showQueueSheet(colorScheme),
|
||||
),
|
||||
@@ -183,20 +184,20 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => const [
|
||||
itemBuilder: (menuContext) => [
|
||||
PopupMenuItem(
|
||||
value: 'details',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.info_outline),
|
||||
title: Text('Details'),
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(menuContext.l10n.nowPlayingDetails),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'external',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.open_in_new),
|
||||
title: Text('Open in external player'),
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(menuContext.l10n.nowPlayingOpenInExternalPlayer),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
@@ -219,7 +220,10 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
_PageTabBar(
|
||||
controller: _pageController,
|
||||
colorScheme: colorScheme,
|
||||
labels: const ['Player', 'Lyrics'],
|
||||
labels: [
|
||||
context.l10n.nowPlayingTabPlayer,
|
||||
context.l10n.nowPlayingTabLyrics,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
@@ -415,7 +419,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'No lyrics in this file',
|
||||
context.l10n.nowPlayingNoLyrics,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -448,7 +452,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Cannot open file: $e')));
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.snackbarCannotOpenFile(e.toString()))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +468,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Your library is empty')));
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.nowPlayingLibraryEmpty)));
|
||||
return;
|
||||
}
|
||||
media.shuffle();
|
||||
@@ -475,7 +479,9 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Could not shuffle library: $e')));
|
||||
).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.nowPlayingShuffleLibraryFailed(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,7 +518,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Up next',
|
||||
context.l10n.nowPlayingUpNext,
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
@@ -521,8 +527,8 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: shuffleOn
|
||||
? 'Shuffle on'
|
||||
: 'Play in order',
|
||||
? context.l10n.nowPlayingShuffleOn
|
||||
: context.l10n.nowPlayingPlayInOrder,
|
||||
isSelected: shuffleOn,
|
||||
icon: const Icon(Icons.shuffle),
|
||||
color: shuffleOn ? colorScheme.primary : null,
|
||||
@@ -539,7 +545,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => _shuffleLibrary(controller),
|
||||
icon: const Icon(Icons.shuffle, size: 18),
|
||||
label: const Text('Shuffle library'),
|
||||
label: Text(context.l10n.nowPlayingShuffleLibrary),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -547,7 +553,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Queue is empty',
|
||||
context.l10n.nowPlayingQueueEmpty,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -652,7 +658,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
if (meta == null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No metadata available',
|
||||
context.l10n.nowPlayingNoMetadata,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -840,29 +846,30 @@ class _MetadataList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String s(Object? v) => (v ?? '').toString();
|
||||
final l10n = context.l10n;
|
||||
final rows = <(String, String)>[
|
||||
('Title', s(meta['title'])),
|
||||
('Artist', s(meta['artist'])),
|
||||
('Album', s(meta['album'])),
|
||||
('Album artist', s(meta['album_artist'])),
|
||||
('Genre', s(meta['genre'])),
|
||||
('Composer', s(meta['composer'])),
|
||||
('Date', s(meta['date'])),
|
||||
('Track', s(meta['track_number'])),
|
||||
('Disc', s(meta['disc_number'])),
|
||||
('ISRC', s(meta['isrc'])),
|
||||
('Label', s(meta['label'])),
|
||||
('Copyright', s(meta['copyright'])),
|
||||
('Format', s(meta['format']).toUpperCase()),
|
||||
('Codec', s(meta['audio_codec'])),
|
||||
(l10n.editMetadataFieldTitle, s(meta['title'])),
|
||||
(l10n.editMetadataFieldArtist, s(meta['artist'])),
|
||||
(l10n.editMetadataFieldAlbum, s(meta['album'])),
|
||||
(l10n.editMetadataFieldAlbumArtist, s(meta['album_artist'])),
|
||||
(l10n.editMetadataFieldGenre, s(meta['genre'])),
|
||||
(l10n.editMetadataFieldComposer, s(meta['composer'])),
|
||||
(l10n.editMetadataFieldDate, s(meta['date'])),
|
||||
(l10n.editMetadataFieldTrackNum, s(meta['track_number'])),
|
||||
(l10n.editMetadataFieldDiscNum, s(meta['disc_number'])),
|
||||
(l10n.editMetadataFieldIsrc, s(meta['isrc'])),
|
||||
(l10n.editMetadataFieldLabel, s(meta['label'])),
|
||||
(l10n.editMetadataFieldCopyright, s(meta['copyright'])),
|
||||
(l10n.libraryFilterFormat, s(meta['format']).toUpperCase()),
|
||||
(l10n.audioAnalysisCodec, s(meta['audio_codec'])),
|
||||
(
|
||||
'Sample rate',
|
||||
l10n.audioAnalysisSampleRate,
|
||||
meta['sample_rate'] != null && (meta['sample_rate'] as num? ?? 0) > 0
|
||||
? '${((meta['sample_rate'] as num) / 1000).toStringAsFixed(1)} kHz'
|
||||
: '',
|
||||
),
|
||||
(
|
||||
'Bit depth',
|
||||
l10n.audioAnalysisBitDepth,
|
||||
(meta['bit_depth'] as num? ?? 0) > 0 ? '${meta['bit_depth']}-bit' : '',
|
||||
),
|
||||
].where((r) => r.$2.trim().isNotEmpty && r.$2 != '0').toList();
|
||||
@@ -896,7 +903,7 @@ class _MetadataList extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Details',
|
||||
context.l10n.nowPlayingDetails,
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
|
||||
@@ -452,73 +452,6 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: 'Playback'),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
icon: Icons.open_in_new,
|
||||
title: 'External player',
|
||||
subtitle:
|
||||
'Open tracks in another music app (recommended for best quality)',
|
||||
trailing: settings.playerMode == 'external'
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () =>
|
||||
ref.read(settingsProvider.notifier).setPlayerMode('external'),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.play_circle_outline,
|
||||
title: 'Built-in player',
|
||||
subtitle:
|
||||
'Play inside SpotiFLAC with a notification and synced lyrics',
|
||||
trailing: settings.playerMode == 'internal'
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () =>
|
||||
ref.read(settingsProvider.notifier).setPlayerMode('internal'),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.tertiaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'The built-in player is intentionally simple (local files '
|
||||
'only, basic playback). For higher quality, gapless audio, '
|
||||
'equalizer and format support, a dedicated external player '
|
||||
'is more capable and recommended.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (settings.localLibraryEnabled) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.libraryActions),
|
||||
@@ -687,6 +620,70 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.libraryPlayback),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsItem(
|
||||
icon: Icons.open_in_new,
|
||||
title: context.l10n.libraryExternalPlayer,
|
||||
subtitle: context.l10n.libraryExternalPlayerSubtitle,
|
||||
trailing: settings.playerMode == 'external'
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setPlayerMode('external'),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.play_circle_outline,
|
||||
title: context.l10n.libraryBuiltInPreviewPlayer,
|
||||
subtitle: context.l10n.libraryBuiltInPreviewPlayerSubtitle,
|
||||
trailing: settings.playerMode == 'internal'
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setPlayerMode('internal'),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.tertiaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.l10n.libraryBuiltInPlayerInfo,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -10,6 +10,17 @@ import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('MusicPlayer');
|
||||
|
||||
String _playbackUnknownTitle = 'Unknown title';
|
||||
String _playbackUnknownArtist = 'Unknown artist';
|
||||
|
||||
void updateMusicPlayerStrings({
|
||||
required String unknownTitle,
|
||||
required String unknownArtist,
|
||||
}) {
|
||||
_playbackUnknownTitle = unknownTitle;
|
||||
_playbackUnknownArtist = unknownArtist;
|
||||
}
|
||||
|
||||
final AudioContext _musicAudioContext = AudioContext(
|
||||
android: const AudioContextAndroid(
|
||||
audioFocus: AndroidAudioFocus.none,
|
||||
@@ -43,8 +54,8 @@ class PlayableMedia {
|
||||
MediaItem toMediaItem({String? resolvedSource}) {
|
||||
return MediaItem(
|
||||
id: id,
|
||||
title: title.isEmpty ? 'Unknown title' : title,
|
||||
artist: artist.isEmpty ? 'Unknown artist' : artist,
|
||||
title: title.isEmpty ? _playbackUnknownTitle : title,
|
||||
artist: artist.isEmpty ? _playbackUnknownArtist : artist,
|
||||
album: album.isEmpty ? null : album,
|
||||
duration: duration,
|
||||
artUri: (artUri != null && artUri!.isNotEmpty)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/services/app_remote_config_service.dart';
|
||||
|
||||
class AppAnnouncementDialog extends StatelessWidget {
|
||||
@@ -49,7 +50,7 @@ class AppAnnouncementDialog extends StatelessWidget {
|
||||
void _showCtaOpenFailed(BuildContext context) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
|
||||
const SnackBar(content: Text('Unable to open link. Please try again.')),
|
||||
SnackBar(content: Text(context.l10n.announcementUnableToOpenLink)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user