mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
- SettingsGroup centers itself at 720dp via its incoming constraint (not screen width, so groups inside clamped sheets are never over-inset; unbounded-width contexts guarded); priority scaffold, bespoke hero cards, and donate page get the same inset - all 18+ settings pages clamp from shared widgets - queue list-mode, local/downloaded album track lists, favorite artists, home recent-access, and extension detail sections adopt wideListInset like the screens fixed earlier - track metadata content clamped at 720dp, which also tames the width-derived spectrogram height - announcement dialog capped at 480dp wide; setup wizard and tutorial content at 560dp with a 400dp action button
353 lines
11 KiB
Dart
353 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:spotiflac_android/l10n/l10n.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<LyricsProviderPriorityPage> createState() =>
|
|
_LyricsProviderPriorityPageState();
|
|
}
|
|
|
|
class _LyricsProviderPriorityPageState
|
|
extends ConsumerState<LyricsProviderPriorityPage> {
|
|
static const _allProviderIds = [
|
|
'lrclib',
|
|
'netease',
|
|
'musixmatch',
|
|
'apple_music',
|
|
'qqmusic',
|
|
'spotify',
|
|
'deezer',
|
|
'youtube',
|
|
'kugou',
|
|
'genius',
|
|
'lyricsplus',
|
|
];
|
|
|
|
late List<String> _enabledProviders;
|
|
late List<String> _initialProviders;
|
|
bool _hasChanges = false;
|
|
|
|
List<String> get _disabledProviders =>
|
|
_allProviderIds.where((id) => !_enabledProviders.contains(id)).toList();
|
|
|
|
@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 disabled = _disabledProviders;
|
|
|
|
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);
|
|
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);
|
|
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<void> _saveChanges() async {
|
|
final settingsNotifier = ref.read(settingsProvider.notifier);
|
|
settingsNotifier.setLyricsProviders(List<String>.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,
|
|
) {
|
|
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: 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,
|
|
});
|
|
}
|