import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/widgets/discard_changes_dialog.dart'; import 'package:spotiflac_android/widgets/priority_settings_scaffold.dart'; import 'package:spotiflac_android/widgets/reorderable_priority_item.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/constants/music_services.dart'; class LyricsProviderPriorityPage extends ConsumerStatefulWidget { const LyricsProviderPriorityPage({super.key}); @override ConsumerState createState() => _LyricsProviderPriorityPageState(); } class _LyricsProviderPriorityPageState extends ConsumerState { static const _builtInProviderIds = [ 'lrclib', 'netease', 'musixmatch', 'apple_music', 'qqmusic', 'spotify', 'deezer', 'youtube', 'kugou', 'genius', 'lyricsplus', ]; late List _enabledProviders; late List _initialProviders; bool _hasChanges = false; @override void initState() { super.initState(); final settings = ref.read(settingsProvider); _enabledProviders = List.from(settings.lyricsProviders); _initialProviders = List.from(settings.lyricsProviders); } void _markChanged() { final changed = _enabledProviders.length != _initialProviders.length || !_enabledProviders.asMap().entries.every( (e) => e.key < _initialProviders.length && _initialProviders[e.key] == e.value, ); setState(() => _hasChanges = changed); } @override Widget build(BuildContext context) { final extensions = ref.watch(extensionProvider).extensions; final extensionNames = { for (final extension in extensions) if (extension.enabled && extension.hasLyricsProvider) 'extension:${extension.id.toLowerCase()}': extension.displayName, }; final allProviderIds = [ ..._builtInProviderIds, ...extensionNames.keys.where((id) => !_builtInProviderIds.contains(id)), ]; final disabled = allProviderIds .where((id) => !_enabledProviders.contains(id)) .toList(); return PrioritySettingsScaffold( hasChanges: _hasChanges, title: context.l10n.lyricsProvidersTitle, description: context.l10n.lyricsProvidersDescription, infoText: context.l10n.lyricsProvidersInfoText, onSave: _saveChanges, onConfirmDiscard: (ctx) => showDiscardChangesDialog( ctx, content: ctx.l10n.lyricsProvidersDiscardContent, ), slivers: [ if (_enabledProviders.isNotEmpty) SliverToBoxAdapter( child: SettingsSectionHeader( title: context.l10n.lyricsProvidersEnabledSection( _enabledProviders.length, ), ), ), if (_enabledProviders.isNotEmpty) SliverPadding( padding: EdgeInsets.symmetric( horizontal: 16 + wideListInset(context), ), sliver: SliverReorderableList( itemCount: _enabledProviders.length, itemBuilder: (context, index) { final id = _enabledProviders[index]; final info = _getLyricsProviderInfo( id, context, extensionNames[id], ); return ReorderablePriorityItem( key: ValueKey(id), index: index, isFirst: index == 0, icon: info.icon, iconColor: Theme.of(context).colorScheme.primary, name: info.name, subtitle: info.description, trailing: SizedBox( height: 32, child: FittedBox( child: Switch( value: true, onChanged: (_) => _disableProvider(id), ), ), ), ); }, onReorderItem: (oldIndex, newIndex) { setState(() { final item = _enabledProviders.removeAt(oldIndex); _enabledProviders.insert(newIndex, item); }); _markChanged(); }, ), ), if (disabled.isNotEmpty) SliverToBoxAdapter( child: SettingsSectionHeader( title: context.l10n.lyricsProvidersDisabledSection( disabled.length, ), ), ), if (disabled.isNotEmpty) SliverPadding( padding: EdgeInsets.symmetric( horizontal: 16 + wideListInset(context), ), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { final id = disabled[index]; final info = _getLyricsProviderInfo( id, context, extensionNames[id], ); return _DisabledProviderItem( key: ValueKey(id), providerId: id, info: info, onToggle: () => _enableProvider(id), ); }, childCount: disabled.length), ), ), ], ); } void _enableProvider(String id) { setState(() => _enabledProviders.add(id)); _markChanged(); } void _disableProvider(String id) { if (_enabledProviders.length <= 1) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.lyricsProvidersAtLeastOne)), ); return; } setState(() => _enabledProviders.remove(id)); _markChanged(); } Future _saveChanges() async { final settingsNotifier = ref.read(settingsProvider.notifier); settingsNotifier.setLyricsProviders(List.from(_enabledProviders)); await settingsNotifier.syncLyricsSettingsToBackend(); if (!mounted) return; setState(() { _initialProviders = List.from(_enabledProviders); _hasChanges = false; }); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(context.l10n.lyricsProvidersSaved))); } static _LyricsProviderInfo _getLyricsProviderInfo( String id, BuildContext context, String? extensionDisplayName, ) { switch (id) { case 'lrclib': return _LyricsProviderInfo( name: 'LRCLIB', description: context.l10n.lyricsProviderLrclibDesc, icon: Icons.subtitles_outlined, ); case 'netease': return _LyricsProviderInfo( name: 'Netease', description: context.l10n.lyricsProviderNeteaseDesc, icon: Icons.cloud_outlined, ); case 'musixmatch': return _LyricsProviderInfo( name: 'Musixmatch', description: context.l10n.lyricsProviderMusixmatchDesc, icon: Icons.translate, ); case 'apple_music': return _LyricsProviderInfo( name: 'Apple Music', description: context.l10n.lyricsProviderAppleMusicDesc, icon: Icons.music_note, ); case 'qqmusic': return _LyricsProviderInfo( name: 'QQ Music', description: context.l10n.lyricsProviderQqMusicDesc, icon: Icons.queue_music, ); case MusicServices.spotify: return _LyricsProviderInfo( name: 'Spotify', description: context.l10n.lyricsProviderExtensionDesc, icon: Icons.graphic_eq, ); case MusicServices.deezer: return _LyricsProviderInfo( name: 'Deezer', description: context.l10n.lyricsProviderExtensionDesc, icon: Icons.album_outlined, ); case 'youtube': return _LyricsProviderInfo( name: 'YouTube', description: context.l10n.lyricsProviderExtensionDesc, icon: Icons.smart_display_outlined, ); case 'kugou': return _LyricsProviderInfo( name: 'Kugou', description: context.l10n.lyricsProviderExtensionDesc, icon: Icons.library_music_outlined, ); case 'genius': return _LyricsProviderInfo( name: 'Genius', description: context.l10n.lyricsProviderExtensionDesc, icon: Icons.auto_awesome_outlined, ); case 'lyricsplus': return _LyricsProviderInfo( name: 'LyricsPlus', description: context.l10n.lyricsProviderLyricsPlusDesc, icon: Icons.lyrics_outlined, ); default: return _LyricsProviderInfo( name: extensionDisplayName ?? (id.startsWith('extension:') ? id.substring('extension:'.length) : id), description: context.l10n.lyricsProviderExtensionDesc, icon: Icons.extension, ); } } } class _DisabledProviderItem extends StatelessWidget { final String providerId; final _LyricsProviderInfo info; final VoidCallback onToggle; const _DisabledProviderItem({ super.key, required this.providerId, required this.info, required this.onToggle, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark; final backgroundColor = isDark ? Color.alphaBlend( Colors.white.withValues(alpha: 0.03), colorScheme.surface, ) : colorScheme.surfaceContainerLow; return Padding( padding: const EdgeInsets.only(bottom: 8), child: Opacity( opacity: 0.6, child: Material( color: backgroundColor, borderRadius: BorderRadius.circular(16), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: onToggle, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ const SizedBox(width: 28), const SizedBox(width: 16), Icon(info.icon, color: colorScheme.outline), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( info.name, style: Theme.of(context).textTheme.bodyLarge ?.copyWith( fontWeight: FontWeight.w500, color: colorScheme.onSurfaceVariant, ), ), Text( info.description, style: Theme.of(context).textTheme.bodySmall ?.copyWith(color: colorScheme.outline), ), ], ), ), SizedBox( height: 32, child: FittedBox( child: Switch(value: false, onChanged: (_) => onToggle()), ), ), ], ), ), ), ), ), ); } } class _LyricsProviderInfo { final String name; final String description; final IconData icon; const _LyricsProviderInfo({ required this.name, required this.description, required this.icon, }); }