mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-07 13:17:57 +02:00
feat: add language selector in Appearance settings
- Add locale field to AppSettings model with 'system' default - Add Language section in Appearance settings page - Support System Default, English, and Indonesian options - App language changes take effect immediately - Add localization strings for language selector
This commit is contained in:
@@ -33,6 +33,13 @@ class SpotiFLACApp extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(_routerProvider);
|
||||
final localeString = ref.watch(settingsProvider.select((s) => s.locale));
|
||||
|
||||
// Convert locale string to Locale object
|
||||
Locale? locale;
|
||||
if (localeString != 'system') {
|
||||
locale = Locale(localeString);
|
||||
}
|
||||
|
||||
return DynamicColorWrapper(
|
||||
builder: (lightTheme, darkTheme, themeMode) {
|
||||
@@ -46,6 +53,7 @@ class SpotiFLACApp extends ConsumerWidget {
|
||||
themeAnimationCurve: Curves.easeInOut,
|
||||
routerConfig: router,
|
||||
// Localization
|
||||
locale: locale, // null = follow system
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
|
||||
@@ -2588,6 +2588,42 @@ abstract class AppLocalizations {
|
||||
/// **'Layout'**
|
||||
String get sectionLayout;
|
||||
|
||||
/// Settings section header for language selection
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Language'**
|
||||
String get sectionLanguage;
|
||||
|
||||
/// Setting title for language selection
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'App Language'**
|
||||
String get appearanceLanguage;
|
||||
|
||||
/// Subtitle for language setting
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose your preferred language'**
|
||||
String get appearanceLanguageSubtitle;
|
||||
|
||||
/// Use device system language
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'System Default'**
|
||||
String get languageSystem;
|
||||
|
||||
/// English language option
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'English'**
|
||||
String get languageEnglish;
|
||||
|
||||
/// Indonesian language option
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bahasa Indonesia'**
|
||||
String get languageIndonesian;
|
||||
|
||||
/// Appearance settings description
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -1428,6 +1428,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get sectionLayout => 'Layout';
|
||||
|
||||
@override
|
||||
String get sectionLanguage => 'Language';
|
||||
|
||||
@override
|
||||
String get appearanceLanguage => 'App Language';
|
||||
|
||||
@override
|
||||
String get appearanceLanguageSubtitle => 'Choose your preferred language';
|
||||
|
||||
@override
|
||||
String get languageSystem => 'System Default';
|
||||
|
||||
@override
|
||||
String get languageEnglish => 'English';
|
||||
|
||||
@override
|
||||
String get languageIndonesian => 'Bahasa Indonesia';
|
||||
|
||||
@override
|
||||
String get settingsAppearanceSubtitle => 'Theme, colors, display';
|
||||
|
||||
|
||||
@@ -1438,6 +1438,24 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get sectionLayout => 'Tata Letak';
|
||||
|
||||
@override
|
||||
String get sectionLanguage => 'Bahasa';
|
||||
|
||||
@override
|
||||
String get appearanceLanguage => 'Bahasa Aplikasi';
|
||||
|
||||
@override
|
||||
String get appearanceLanguageSubtitle => 'Pilih bahasa yang kamu inginkan';
|
||||
|
||||
@override
|
||||
String get languageSystem => 'Bawaan Sistem';
|
||||
|
||||
@override
|
||||
String get languageEnglish => 'English';
|
||||
|
||||
@override
|
||||
String get languageIndonesian => 'Bahasa Indonesia';
|
||||
|
||||
@override
|
||||
String get settingsAppearanceSubtitle => 'Tema, warna, tampilan';
|
||||
|
||||
|
||||
@@ -1046,6 +1046,19 @@
|
||||
"@sectionTheme": {"description": "Settings section header"},
|
||||
"sectionLayout": "Layout",
|
||||
"@sectionLayout": {"description": "Settings section header"},
|
||||
"sectionLanguage": "Language",
|
||||
"@sectionLanguage": {"description": "Settings section header for language selection"},
|
||||
|
||||
"appearanceLanguage": "App Language",
|
||||
"@appearanceLanguage": {"description": "Setting title for language selection"},
|
||||
"appearanceLanguageSubtitle": "Choose your preferred language",
|
||||
"@appearanceLanguageSubtitle": {"description": "Subtitle for language setting"},
|
||||
"languageSystem": "System Default",
|
||||
"@languageSystem": {"description": "Use device system language"},
|
||||
"languageEnglish": "English",
|
||||
"@languageEnglish": {"description": "English language option"},
|
||||
"languageIndonesian": "Bahasa Indonesia",
|
||||
"@languageIndonesian": {"description": "Indonesian language option"},
|
||||
|
||||
"settingsAppearanceSubtitle": "Theme, colors, display",
|
||||
"@settingsAppearanceSubtitle": {"description": "Appearance settings description"},
|
||||
|
||||
@@ -305,6 +305,13 @@
|
||||
"sectionColor": "Warna",
|
||||
"sectionTheme": "Tema",
|
||||
"sectionLayout": "Tata Letak",
|
||||
"sectionLanguage": "Bahasa",
|
||||
|
||||
"appearanceLanguage": "Bahasa Aplikasi",
|
||||
"appearanceLanguageSubtitle": "Pilih bahasa yang kamu inginkan",
|
||||
"languageSystem": "Bawaan Sistem",
|
||||
"languageEnglish": "English",
|
||||
"languageIndonesian": "Bahasa Indonesia",
|
||||
|
||||
"settingsAppearanceSubtitle": "Tema, warna, tampilan",
|
||||
"settingsDownloadSubtitle": "Layanan, kualitas, format nama file",
|
||||
|
||||
@@ -30,6 +30,7 @@ class AppSettings {
|
||||
final bool separateSingles; // Separate singles/EPs into their own folder
|
||||
final String albumFolderStructure; // artist_album, album_only, artist_year_album, year_album
|
||||
final bool showExtensionStore; // Show Extension Store tab in navigation
|
||||
final String locale; // App language: 'system', 'en', 'id', etc.
|
||||
|
||||
const AppSettings({
|
||||
this.defaultService = 'tidal',
|
||||
@@ -58,6 +59,7 @@ class AppSettings {
|
||||
this.separateSingles = false, // Default: disabled
|
||||
this.albumFolderStructure = 'artist_album', // Default: Albums/Artist/Album
|
||||
this.showExtensionStore = true, // Default: show store
|
||||
this.locale = 'system', // Default: follow system language
|
||||
});
|
||||
|
||||
AppSettings copyWith({
|
||||
@@ -88,6 +90,7 @@ class AppSettings {
|
||||
bool? separateSingles,
|
||||
String? albumFolderStructure,
|
||||
bool? showExtensionStore,
|
||||
String? locale,
|
||||
}) {
|
||||
return AppSettings(
|
||||
defaultService: defaultService ?? this.defaultService,
|
||||
@@ -116,6 +119,7 @@ class AppSettings {
|
||||
separateSingles: separateSingles ?? this.separateSingles,
|
||||
albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure,
|
||||
showExtensionStore: showExtensionStore ?? this.showExtensionStore,
|
||||
locale: locale ?? this.locale,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
albumFolderStructure:
|
||||
json['albumFolderStructure'] as String? ?? 'artist_album',
|
||||
showExtensionStore: json['showExtensionStore'] as bool? ?? true,
|
||||
locale: json['locale'] as String? ?? 'system',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
|
||||
@@ -65,4 +66,5 @@ Map<String, dynamic> _$AppSettingsToJson(AppSettings instance) =>
|
||||
'separateSingles': instance.separateSingles,
|
||||
'albumFolderStructure': instance.albumFolderStructure,
|
||||
'showExtensionStore': instance.showExtensionStore,
|
||||
'locale': instance.locale,
|
||||
};
|
||||
|
||||
@@ -230,6 +230,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
state = state.copyWith(showExtensionStore: enabled);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setLocale(String locale) {
|
||||
state = state.copyWith(locale: locale);
|
||||
_saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
final settingsProvider = NotifierProvider<SettingsNotifier, AppSettings>(
|
||||
|
||||
@@ -108,6 +108,23 @@ class AppearanceSettingsPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Language section
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionLanguage),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
_LanguageSelector(
|
||||
currentLocale: settings.locale,
|
||||
onChanged: (locale) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setLocale(locale),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Layout section
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionLayout),
|
||||
@@ -683,3 +700,139 @@ class _ViewModeChip extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LanguageSelector extends StatelessWidget {
|
||||
final String currentLocale;
|
||||
final ValueChanged<String> onChanged;
|
||||
const _LanguageSelector({
|
||||
required this.currentLocale,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, bottom: 8),
|
||||
child: Text(
|
||||
context.l10n.appearanceLanguage,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_LanguageChip(
|
||||
icon: Icons.phone_android,
|
||||
label: context.l10n.languageSystem,
|
||||
isSelected: currentLocale == 'system',
|
||||
onTap: () => onChanged('system'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_LanguageChip(
|
||||
icon: Icons.language,
|
||||
label: context.l10n.languageEnglish,
|
||||
isSelected: currentLocale == 'en',
|
||||
onTap: () => onChanged('en'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_LanguageChip(
|
||||
icon: Icons.language,
|
||||
label: context.l10n.languageIndonesian,
|
||||
isSelected: currentLocale == 'id',
|
||||
onTap: () => onChanged('id'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LanguageChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
const _LanguageChip({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
final unselectedColor = isDark
|
||||
? Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.05),
|
||||
colorScheme.surface,
|
||||
)
|
||||
: Color.alphaBlend(
|
||||
Colors.black.withValues(alpha: 0.05),
|
||||
colorScheme.surfaceContainerHighest,
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? colorScheme.primaryContainer : unselectedColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: !isDark && !isSelected
|
||||
? Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user