import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/l10n/supported_locales.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/theme_provider.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/app_sliver_header.dart'; class AppearanceSettingsPage extends ConsumerWidget { const AppearanceSettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final themeSettings = ref.watch(themeProvider); final settings = ref.watch(settingsProvider); return PopScope( canPop: true, child: Scaffold( body: CustomScrollView( slivers: [ AppSliverHeader.page(title: context.l10n.appearanceTitle), SliverToBoxAdapter( child: Padding( padding: EdgeInsets.symmetric( horizontal: 16 + wideListInset(context), vertical: 8, ), child: _ThemePreviewCard(), ), ), SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionColor), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.wallpaper, title: context.l10n.appearanceDynamicColor, subtitle: context.l10n.appearanceDynamicColorSubtitle, value: themeSettings.useDynamicColor, onChanged: (value) => ref .read(themeProvider.notifier) .setUseDynamicColor(value), showDivider: false, ), ], ), ), if (!themeSettings.useDynamicColor) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: _ColorPalettePicker( currentColor: themeSettings.seedColorValue, onColorSelected: (color) => ref.read(themeProvider.notifier).setSeedColor(color), ), ), ), SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionTheme), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _ThemeModeSelector( currentMode: themeSettings.themeMode, onChanged: (mode) => ref.read(themeProvider.notifier).setThemeMode(mode), ), if (Theme.of(context).brightness == Brightness.dark) SettingsSwitchItem( icon: Icons.brightness_2, title: context.l10n.appearanceAmoledDark, subtitle: context.l10n.appearanceAmoledDarkSubtitle, value: themeSettings.useAmoled, onChanged: (value) => ref.read(themeProvider.notifier).setUseAmoled(value), showDivider: false, ), ], ), ), SliverToBoxAdapter( child: SettingsGroup( children: [ SettingsSwitchItem( icon: Icons.animation, title: context.l10n.appearanceHeroAnimations, subtitle: context.l10n.appearanceHeroAnimationsSubtitle, value: settings.heroAnimationsEnabled, onChanged: (value) => ref .read(settingsProvider.notifier) .setHeroAnimationsEnabled(value), ), SettingsSwitchItem( icon: Icons.blur_on, title: context.l10n.appearanceForceBlur, subtitle: context.l10n.appearanceForceBlurSubtitle, value: settings.forceBackdropBlur, onChanged: (value) => ref .read(settingsProvider.notifier) .setForceBackdropBlur(value), showDivider: false, ), ], ), ), SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLanguage), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _LanguageSelector( currentLocale: settings.locale, onChanged: (locale) => ref.read(settingsProvider.notifier).setLocale(locale), ), ], ), ), SliverToBoxAdapter( child: SettingsSectionHeader(title: context.l10n.sectionLayout), ), SliverToBoxAdapter( child: SettingsGroup( children: [ _HistoryViewSelector( currentMode: settings.historyViewMode, onChanged: (mode) => ref .read(settingsProvider.notifier) .setHistoryViewMode(mode), ), ], ), ), const SliverFillRemaining( hasScrollBody: false, child: SizedBox(height: 32), ), ], ), ), ); } } class _ThemePreviewCard extends StatelessWidget { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; return RepaintBoundary( child: LayoutBuilder( builder: (context, constraints) { final cardWidth = constraints.maxWidth; final previewHeight = (cardWidth * 0.56).clamp(170.0, 220.0); final innerWidth = (cardWidth - 48).clamp(220.0, 320.0); final innerHeight = (previewHeight * 0.70).clamp(120.0, 160.0); final innerPadding = (innerHeight * 0.11).clamp(12.0, 18.0); final artworkSize = (innerHeight - (innerPadding * 2)).clamp( 80.0, 120.0, ); return Container( constraints: BoxConstraints(minHeight: previewHeight), width: double.infinity, decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(28), ), clipBehavior: Clip.antiAlias, child: Stack( children: [ Positioned( top: -(previewHeight * 0.25), right: -(previewHeight * 0.25), child: Container( width: previewHeight, height: previewHeight, decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.primaryContainer.withValues( alpha: 0.5, ), ), ), ), Positioned( bottom: -(previewHeight * 0.15), left: -(previewHeight * 0.15), child: Container( width: previewHeight * 0.75, height: previewHeight * 0.75, decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.tertiaryContainer.withValues( alpha: 0.5, ), ), ), ), Center( child: Container( width: innerWidth, height: innerHeight, padding: EdgeInsets.all(innerPadding), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 12, offset: const Offset(0, 8), ), ], ), child: Row( children: [ Container( width: artworkSize, height: artworkSize, decoration: BoxDecoration( color: colorScheme.primary, borderRadius: BorderRadius.circular(16), ), child: Icon( Icons.music_note, color: colorScheme.onPrimary, size: artworkSize * 0.44, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: double.infinity, height: 14, decoration: BoxDecoration( color: colorScheme.onSurface, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(height: 8), Container( width: 80, height: 10, decoration: BoxDecoration( color: colorScheme.primary, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(height: 24), Row( children: [ Icon( Icons.skip_previous, size: 24, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: 12), Icon( Icons.play_circle_fill, size: 32, color: colorScheme.primary, ), const SizedBox(width: 12), Icon( Icons.skip_next, size: 24, color: colorScheme.onSurfaceVariant, ), ], ), ], ), ), ], ), ), ), Positioned( bottom: 12, right: 12, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(20), ), child: Text( isDark ? context.l10n.appearanceThemeDark : context.l10n.appearanceThemeLight, style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ), ], ), ); }, ), ); } } class _ColorPalettePicker extends StatelessWidget { final int currentColor; final ValueChanged onColorSelected; const _ColorPalettePicker({ required this.currentColor, required this.onColorSelected, }); static const _colors = [ Color(0xFF1DB954), Color(0xFF6750A4), Color(0xFF0061A4), Color(0xFF006E1C), Color(0xFFBA1A1A), Color(0xFF984061), Color(0xFF7D5260), Color(0xFF006874), ]; @override Widget build(BuildContext context) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _colors.map((color) { final isSelected = color.toARGB32() == currentColor; final colorHex = color .toARGB32() .toRadixString(16) .padLeft(8, '0') .toUpperCase(); return Padding( padding: const EdgeInsets.only(right: 12), child: Semantics( button: true, selected: isSelected, label: context.l10n.appearanceSelectAccentColor(colorHex), child: GestureDetector( onTap: () => onColorSelected(color), child: _ColorPaletteItem(color: color, isSelected: isSelected), ), ), ); }).toList(), ), ); } } class _ColorPaletteItem extends StatelessWidget { final Color color; final bool isSelected; const _ColorPaletteItem({required this.color, required this.isSelected}); @override Widget build(BuildContext context) { final scheme = ColorScheme.fromSeed( seedColor: color, brightness: Theme.of(context).brightness, ); final size = 64.0; return Stack( children: [ Container( width: size, height: size, decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)), clipBehavior: Clip.antiAlias, child: Column( children: [ Expanded( child: Row( children: [ Expanded(child: Container(color: scheme.primaryContainer)), Expanded(child: Container(color: scheme.tertiaryContainer)), ], ), ), Expanded( child: Row( children: [ Expanded( child: Container(color: scheme.secondaryContainer), ), Expanded(child: Container(color: scheme.surfaceContainer)), ], ), ), ], ), ), if (isSelected) Positioned.fill( child: Center( child: Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), child: Icon(Icons.check, size: 16, color: scheme.primary), ), ), ), ], ); } } class _ThemeModeSelector extends StatelessWidget { final ThemeMode currentMode; final ValueChanged onChanged; const _ThemeModeSelector({ required this.currentMode, required this.onChanged, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(12), child: Row( children: [ SettingsChoiceChip( expand: true, layout: SettingsChipLayout.column, icon: Icons.brightness_auto, label: context.l10n.appearanceThemeSystem, isSelected: currentMode == ThemeMode.system, onTap: () => onChanged(ThemeMode.system), ), const SizedBox(width: 8), SettingsChoiceChip( expand: true, layout: SettingsChipLayout.column, icon: Icons.light_mode, label: context.l10n.appearanceThemeLight, isSelected: currentMode == ThemeMode.light, onTap: () => onChanged(ThemeMode.light), ), const SizedBox(width: 8), SettingsChoiceChip( expand: true, layout: SettingsChipLayout.column, icon: Icons.dark_mode, label: context.l10n.appearanceThemeDark, isSelected: currentMode == ThemeMode.dark, onTap: () => onChanged(ThemeMode.dark), ), ], ), ); } } class _HistoryViewSelector extends StatelessWidget { final String currentMode; final ValueChanged onChanged; const _HistoryViewSelector({ required this.currentMode, 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.appearanceHistoryView, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), Row( children: [ SettingsChoiceChip( expand: true, layout: SettingsChipLayout.column, icon: Icons.view_list, label: context.l10n.appearanceHistoryViewList, isSelected: currentMode == 'list', onTap: () => onChanged('list'), ), const SizedBox(width: 8), SettingsChoiceChip( expand: true, layout: SettingsChipLayout.column, icon: Icons.grid_view, label: context.l10n.appearanceHistoryViewGrid, isSelected: currentMode == 'grid', onTap: () => onChanged('grid'), ), ], ), ], ), ); } } class _LanguageSelector extends StatelessWidget { final String currentLocale; final ValueChanged onChanged; const _LanguageSelector({ required this.currentLocale, required this.onChanged, }); static const _allLanguages = [ ('system', 'System Default', Icons.phone_android), ('en', 'English', Icons.language), ('id', 'Bahasa Indonesia', Icons.language), ('de', 'Deutsch', Icons.language), ('es', 'Español', Icons.language), ('es_ES', 'Español (España)', Icons.language), ('fr', 'Français', Icons.language), ('ja', '日本語', Icons.language), ('ko', '한국어', Icons.language), ('pt', 'Português', Icons.language), ('pt_PT', 'Português (Brasil)', Icons.language), ('ru', 'Русский', Icons.language), ('tr', 'Türkçe', Icons.language), ('uk', 'Українська', Icons.language), ]; /// Uses filteredLocaleCodes from supported_locales.dart (generated file). List<(String, String, IconData)> get _languages { return _allLanguages.where((lang) { if (lang.$1 == 'system') return true; return filteredLocaleCodes.contains(lang.$1); }).toList(); } String _getLanguageName(String code) { for (final lang in _allLanguages) { if (lang.$1 == code) return lang.$2; } return code; } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return ListTile( leading: Icon(Icons.language, color: colorScheme.onSurfaceVariant), title: Text(context.l10n.appearanceLanguage), subtitle: Text(_getLanguageName(currentLocale)), trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), onTap: () => _showLanguagePicker(context), ); } void _showLanguagePicker(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surface, builder: (context) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.all(16), child: Text( context.l10n.appearanceLanguage, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), const Divider(height: 1), Flexible( child: ListView.builder( shrinkWrap: true, itemCount: _languages.length, itemBuilder: (context, index) { final lang = _languages[index]; final isSelected = currentLocale == lang.$1; return ListTile( leading: Icon( lang.$3, color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant, ), title: Text( lang.$2, style: TextStyle( color: isSelected ? colorScheme.primary : colorScheme.onSurface, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), trailing: isSelected ? Icon(Icons.check, color: colorScheme.primary) : null, onTap: () { onChanged(lang.$1); Navigator.pop(context); }, ); }, ), ), const SizedBox(height: 8), ], ), ), ); } }