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/app_bar_layout.dart'; import 'package:spotiflac_android/widgets/settings_group.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); final colorScheme = Theme.of(context).colorScheme; final topPadding = normalizedHeaderTopPadding(context); return PopScope( canPop: true, child: Scaffold( body: CustomScrollView( slivers: [ SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, floating: false, pinned: true, backgroundColor: colorScheme.surface, surfaceTintColor: Colors.transparent, leading: IconButton( tooltip: MaterialLocalizations.of(context).backButtonTooltip, icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), ), flexibleSpace: _AppBarTitle( title: context.l10n.appearanceTitle, topPadding: topPadding, ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, 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: 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 _AppBarTitle extends StatelessWidget { final String title; final double topPadding; const _AppBarTitle({required this.title, required this.topPadding}); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return LayoutBuilder( builder: (context, constraints) { final maxHeight = 120 + topPadding; final minHeight = kToolbarHeight + topPadding; final expandRatio = ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)) .clamp(0.0, 1.0); final leftPadding = 56 - (32 * expandRatio); return FlexibleSpaceBar( expandedTitleScale: 1.0, titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16), title: Text( title, style: TextStyle( fontSize: 20 + (8 * expandRatio), fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ); }, ); } } 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: [ _ThemeModeChip( icon: Icons.brightness_auto, label: context.l10n.appearanceThemeSystem, isSelected: currentMode == ThemeMode.system, onTap: () => onChanged(ThemeMode.system), ), const SizedBox(width: 8), _ThemeModeChip( icon: Icons.light_mode, label: context.l10n.appearanceThemeLight, isSelected: currentMode == ThemeMode.light, onTap: () => onChanged(ThemeMode.light), ), const SizedBox(width: 8), _ThemeModeChip( icon: Icons.dark_mode, label: context.l10n.appearanceThemeDark, isSelected: currentMode == ThemeMode.dark, onTap: () => onChanged(ThemeMode.dark), ), ], ), ); } } class _ThemeModeChip extends StatelessWidget { final IconData icon; final String label; final bool isSelected; final VoidCallback onTap; const _ThemeModeChip({ 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: 12, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, ), ), ], ), ), ), ), ), ); } } 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: [ _ViewModeChip( icon: Icons.view_list, label: context.l10n.appearanceHistoryViewList, isSelected: currentMode == 'list', onTap: () => onChanged('list'), ), const SizedBox(width: 8), _ViewModeChip( icon: Icons.grid_view, label: context.l10n.appearanceHistoryViewGrid, isSelected: currentMode == 'grid', onTap: () => onChanged('grid'), ), ], ), ], ), ); } } class _ViewModeChip extends StatelessWidget { final IconData icon; final String label; final bool isSelected; final VoidCallback onTap; const _ViewModeChip({ 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: 12, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, color: isSelected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, ), ), ], ), ), ), ), ), ); } } 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), ('hi', 'हिन्दी', Icons.language), ('ja', '日本語', Icons.language), ('ko', '한국어', Icons.language), ('nl', 'Nederlands', 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), ('zh', '简体中文', Icons.language), ('zh_CN', '简体中文 (中国)', Icons.language), ('zh_TW', '繁體中文', 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, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), 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), ], ), ), ); } }