Able to adjust lyrics mode and player default mode

This commit is contained in:
2025-12-20 13:07:48 +08:00
parent 9a891a9a98
commit f5c8236363
4 changed files with 363 additions and 57 deletions

View File

@@ -12,16 +12,38 @@ enum ImportMode {
final String displayName;
}
enum DefaultPlayerScreen {
cover('Cover'),
lyrics('Lyrics'),
queue('Queue');
const DefaultPlayerScreen(this.displayName);
final String displayName;
}
enum LyricsMode {
curved('Curved'),
flat('Flat'),
auto('Auto');
const LyricsMode(this.displayName);
final String displayName;
}
class SettingsState {
final ImportMode importMode;
final bool autoScan;
final bool watchForChanges;
final DefaultPlayerScreen defaultPlayerScreen;
final LyricsMode lyricsMode;
final Set<String> supportedFormats;
const SettingsState({
this.importMode = ImportMode.mixed,
this.autoScan = true,
this.watchForChanges = true,
this.defaultPlayerScreen = DefaultPlayerScreen.cover,
this.lyricsMode = LyricsMode.auto,
this.supportedFormats = const {
'.mp3',
'.flac',
@@ -38,12 +60,16 @@ class SettingsState {
ImportMode? importMode,
bool? autoScan,
bool? watchForChanges,
DefaultPlayerScreen? defaultPlayerScreen,
LyricsMode? lyricsMode,
Set<String>? supportedFormats,
}) {
return SettingsState(
importMode: importMode ?? this.importMode,
autoScan: autoScan ?? this.autoScan,
watchForChanges: watchForChanges ?? this.watchForChanges,
defaultPlayerScreen: defaultPlayerScreen ?? this.defaultPlayerScreen,
lyricsMode: lyricsMode ?? this.lyricsMode,
supportedFormats: supportedFormats ?? this.supportedFormats,
);
}
@@ -54,6 +80,8 @@ class SettingsNotifier extends _$SettingsNotifier {
static const String _importModeKey = 'import_mode';
static const String _autoScanKey = 'auto_scan';
static const String _watchForChangesKey = 'watch_for_changes';
static const String _defaultPlayerScreenKey = 'default_player_screen';
static const String _lyricsModeKey = 'lyrics_mode';
@override
Future<SettingsState> build() async {
@@ -65,10 +93,20 @@ class SettingsNotifier extends _$SettingsNotifier {
final autoScan = prefs.getBool(_autoScanKey) ?? true;
final watchForChanges = prefs.getBool(_watchForChangesKey) ?? true;
final defaultPlayerScreenIndex = prefs.getInt(_defaultPlayerScreenKey) ?? 0;
final defaultPlayerScreen =
DefaultPlayerScreen.values[defaultPlayerScreenIndex];
final lyricsModeIndex =
prefs.getInt(_lyricsModeKey) ?? 2; // Auto is default
final lyricsMode = LyricsMode.values[lyricsModeIndex];
return SettingsState(
importMode: importMode,
autoScan: autoScan,
watchForChanges: watchForChanges,
defaultPlayerScreen: defaultPlayerScreen,
lyricsMode: lyricsMode,
);
}
@@ -98,6 +136,26 @@ class SettingsNotifier extends _$SettingsNotifier {
state = AsyncValue.data(state.value!.copyWith(watchForChanges: enabled));
}
}
Future<void> setDefaultPlayerScreen(DefaultPlayerScreen screen) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_defaultPlayerScreenKey, screen.index);
if (state.hasValue) {
state = AsyncValue.data(
state.value!.copyWith(defaultPlayerScreen: screen),
);
}
}
Future<void> setLyricsMode(LyricsMode mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_lyricsModeKey, mode.index);
if (state.hasValue) {
state = AsyncValue.data(state.value!.copyWith(lyricsMode: mode));
}
}
}
// Convenience providers for specific settings
@@ -154,3 +212,39 @@ class WatchForChangesNotifier extends _$WatchForChangesNotifier {
await ref.read(settingsProvider.notifier).setWatchForChanges(enabled);
}
}
@riverpod
class DefaultPlayerScreenNotifier extends _$DefaultPlayerScreenNotifier {
@override
DefaultPlayerScreen build() {
return ref
.watch(settingsProvider)
.when(
data: (settings) => settings.defaultPlayerScreen,
loading: () => DefaultPlayerScreen.cover,
error: (_, _) => DefaultPlayerScreen.cover,
);
}
Future<void> update(DefaultPlayerScreen screen) async {
await ref.read(settingsProvider.notifier).setDefaultPlayerScreen(screen);
}
}
@riverpod
class LyricsModeNotifier extends _$LyricsModeNotifier {
@override
LyricsMode build() {
return ref
.watch(settingsProvider)
.when(
data: (settings) => settings.lyricsMode,
loading: () => LyricsMode.auto,
error: (_, _) => LyricsMode.auto,
);
}
Future<void> update(LyricsMode mode) async {
await ref.read(settingsProvider.notifier).setLyricsMode(mode);
}
}

View File

@@ -33,7 +33,7 @@ final class SettingsNotifierProvider
SettingsNotifier create() => SettingsNotifier();
}
String _$settingsNotifierHash() => r'6dc43c0f1d6ee7b7744dae2a8557b758574473d2';
String _$settingsNotifierHash() => r'4099dd1aa3dfc971c0761f314d196f3bc97315e7';
abstract class _$SettingsNotifier extends $AsyncNotifier<SettingsState> {
FutureOr<SettingsState> build();
@@ -214,3 +214,113 @@ abstract class _$WatchForChangesNotifier extends $Notifier<bool> {
element.handleValue(ref, created);
}
}
@ProviderFor(DefaultPlayerScreenNotifier)
const defaultPlayerScreenProvider = DefaultPlayerScreenNotifierProvider._();
final class DefaultPlayerScreenNotifierProvider
extends
$NotifierProvider<DefaultPlayerScreenNotifier, DefaultPlayerScreen> {
const DefaultPlayerScreenNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'defaultPlayerScreenProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$defaultPlayerScreenNotifierHash();
@$internal
@override
DefaultPlayerScreenNotifier create() => DefaultPlayerScreenNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(DefaultPlayerScreen value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<DefaultPlayerScreen>(value),
);
}
}
String _$defaultPlayerScreenNotifierHash() =>
r'cddfe0510ec38b3d5800901bd018728ca2567d54';
abstract class _$DefaultPlayerScreenNotifier
extends $Notifier<DefaultPlayerScreen> {
DefaultPlayerScreen build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<DefaultPlayerScreen, DefaultPlayerScreen>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<DefaultPlayerScreen, DefaultPlayerScreen>,
DefaultPlayerScreen,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(LyricsModeNotifier)
const lyricsModeProvider = LyricsModeNotifierProvider._();
final class LyricsModeNotifierProvider
extends $NotifierProvider<LyricsModeNotifier, LyricsMode> {
const LyricsModeNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'lyricsModeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$lyricsModeNotifierHash();
@$internal
@override
LyricsModeNotifier create() => LyricsModeNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(LyricsMode value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<LyricsMode>(value),
);
}
}
String _$lyricsModeNotifierHash() =>
r'b3f77739bfab6bb7551cb31bebfef5c8b6dcb423';
abstract class _$LyricsModeNotifier extends $Notifier<LyricsMode> {
LyricsMode build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<LyricsMode, LyricsMode>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<LyricsMode, LyricsMode>,
LyricsMode,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -5,6 +5,7 @@ import 'dart:ui';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:drift/drift.dart' as drift;
import 'package:gap/gap.dart';
@@ -16,6 +17,7 @@ import 'package:groovybox/logic/metadata_service.dart';
import 'package:groovybox/providers/audio_provider.dart';
import 'package:groovybox/providers/db_provider.dart';
import 'package:groovybox/providers/lrc_fetcher_provider.dart';
import 'package:groovybox/providers/settings_provider.dart';
import 'package:groovybox/ui/widgets/mini_player.dart';
import 'package:groovybox/ui/widgets/track_tile.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -35,7 +37,26 @@ class PlayerScreen extends HookConsumerWidget {
final audioHandler = ref.watch(audioHandlerProvider);
final player = audioHandler.player;
final viewMode = useState(ViewMode.cover);
// Use the default player screen setting from main settings provider
final settingsAsync = ref.watch(settingsProvider);
final defaultPlayerScreen = settingsAsync.maybeWhen(
data: (settings) => settings.defaultPlayerScreen,
orElse: () => null, // Return null when loading/error
);
final viewMode = useState<ViewMode>(ViewMode.cover); // Start with cover
// Update viewMode when defaultPlayerScreen setting is loaded
useEffect(() {
if (defaultPlayerScreen != null) {
final newViewMode = switch (defaultPlayerScreen) {
DefaultPlayerScreen.cover => ViewMode.cover,
DefaultPlayerScreen.lyrics => ViewMode.lyrics,
DefaultPlayerScreen.queue => ViewMode.queue,
};
viewMode.value = newViewMode;
}
return null;
}, [defaultPlayerScreen]);
final isMobile = MediaQuery.sizeOf(context).width <= 800;
return StreamBuilder<Playlist>(
@@ -534,54 +555,42 @@ class _PlayerLyrics extends HookConsumerWidget {
dynamic neteaseProviderInstance,
BuildContext context,
) {
// Get lyrics mode setting
final lyricsMode = ref.watch(lyricsModeProvider);
final isDesktop = MediaQuery.sizeOf(context).width > 800;
// Determine if we should use curved (desktop-style) or flat (mobile-style) lyrics
final useCurvedStyle = switch (lyricsMode) {
LyricsMode.curved => true,
LyricsMode.flat => false,
LyricsMode.auto =>
isDesktop, // Auto mode: curved on desktop, flat on mobile
};
if (track.lyrics == null) {
// Show fetch lyrics UI
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'No Lyrics Available',
style: TextStyle(fontStyle: FontStyle.italic, fontSize: 18),
),
Text('No Lyrics Available'),
const SizedBox(height: 16),
ElevatedButton.icon(
icon: const Icon(Symbols.download),
label: const Text('Fetch Lyrics'),
onPressed: () => _showFetchLyricsDialog(
context,
ref,
track,
track.path,
metadataAsync.value,
musixmatchProviderInstance,
neteaseProviderInstance,
),
),
if (lyricsFetcher.isLoading)
Padding(
padding: const EdgeInsets.all(16.0),
child: LinearProgressIndicator(),
),
if (lyricsFetcher.error != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
lyricsFetcher.error!,
style: TextStyle(color: Colors.red, fontSize: 12),
textAlign: TextAlign.center,
),
),
if (lyricsFetcher.successMessage != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
lyricsFetcher.successMessage!,
style: TextStyle(
color: Colors.green,
fontSize: 14,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
child: CircularProgressIndicator(),
)
else
ElevatedButton.icon(
icon: const Icon(Symbols.download),
label: const Text('Fetch Lyrics'),
onPressed: () => _showFetchLyricsDialog(
context,
ref,
track,
track.path,
metadataAsync.value,
musixmatchProviderInstance,
neteaseProviderInstance,
),
),
],
@@ -599,24 +608,29 @@ class _PlayerLyrics extends HookConsumerWidget {
);
} else {
// Plain text lyrics
final isDesktop = MediaQuery.sizeOf(context).width > 800;
if (isDesktop) {
if (useCurvedStyle) {
return ListWheelScrollView.useDelegate(
itemExtent: 50,
perspective: 0.002,
offAxisFraction: 1.5,
perspective: 0.001,
offAxisFraction: isDesktop ? 1.5 : 0,
squeeze: 1.0,
diameterRatio: 2,
diameterRatio: isDesktop
? 2
: RenderListWheelViewport.defaultDiameterRatio,
physics: const FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
childCount: lyricsData.lines.length,
builder: (context, index) {
final line = lyricsData.lines[index];
return Align(
alignment: Alignment.centerRight,
alignment: isDesktop
? Alignment.centerRight
: Alignment.center,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width * 0.4,
maxWidth: isDesktop
? MediaQuery.sizeOf(context).width * 0.4
: MediaQuery.sizeOf(context).width * 0.8,
),
child: Container(
alignment: Alignment.centerLeft,
@@ -1296,8 +1310,18 @@ class _TimedLyricsView extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Get lyrics mode setting
final lyricsMode = ref.watch(lyricsModeProvider);
final isDesktop = MediaQuery.sizeOf(context).width > 800;
// Determine if we should use curved (desktop-style) or flat (mobile-style) lyrics
final useCurvedStyle = switch (lyricsMode) {
LyricsMode.curved => true,
LyricsMode.flat => false,
LyricsMode.auto =>
isDesktop, // Auto mode: curved on desktop, flat on mobile
};
final listController = useMemoized(() => ListController(), []);
final scrollController = useScrollController();
final wheelScrollController = useMemoized(
@@ -1330,7 +1354,7 @@ class _TimedLyricsView extends HookConsumerWidget {
if (currentIndex != previousIndex.value) {
WidgetsBinding.instance.addPostFrameCallback((_) {
previousIndex.value = currentIndex;
if (isDesktop) {
if (useCurvedStyle) {
if (wheelScrollController.hasClients) {
wheelScrollController.animateToItem(
currentIndex,
@@ -1352,14 +1376,16 @@ class _TimedLyricsView extends HookConsumerWidget {
final totalDurationMs = player.state.duration.inMilliseconds;
if (isDesktop) {
if (useCurvedStyle) {
return ListWheelScrollView.useDelegate(
controller: wheelScrollController,
itemExtent: 50,
perspective: 0.002,
offAxisFraction: 1.5,
perspective: 0.001,
offAxisFraction: isDesktop ? 1.5 : 0,
squeeze: 1.0,
diameterRatio: 2,
diameterRatio: isDesktop
? 2
: RenderListWheelViewport.defaultDiameterRatio,
physics: const FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
childCount: lyrics.lines.length,
@@ -1382,10 +1408,14 @@ class _TimedLyricsView extends HookConsumerWidget {
}
return Align(
alignment: Alignment.centerRight,
alignment: isDesktop
? Alignment.centerRight
: Alignment.center,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.sizeOf(context).width * 0.4,
maxWidth: isDesktop
? MediaQuery.sizeOf(context).width * 0.4
: MediaQuery.sizeOf(context).width * 0.8,
),
child: InkWell(
onTap: () {

View File

@@ -42,7 +42,7 @@ class SettingsScreen extends ConsumerWidget {
fontSize: 18,
fontWeight: FontWeight.bold,
),
).padding(horizontal: 16, bottom: 8, top: 16),
).padding(horizontal: 16, top: 16),
SwitchListTile(
title: const Text('Auto-scan music libraries'),
subtitle: const Text(
@@ -312,6 +312,78 @@ class SettingsScreen extends ConsumerWidget {
),
),
// Player Settings Section
Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Player Settings',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
).padding(horizontal: 16, top: 16),
const Text(
'Configure player behavior and display options.',
style: TextStyle(color: Colors.grey, fontSize: 14),
).padding(horizontal: 16, bottom: 8),
ListTile(
title: const Text('Default Player Screen'),
subtitle: const Text(
'Choose which screen to show when opening the player.',
),
trailing: DropdownButtonHideUnderline(
child: DropdownButton<DefaultPlayerScreen>(
value: settings.defaultPlayerScreen,
onChanged: (DefaultPlayerScreen? value) {
if (value != null) {
ref
.read(
defaultPlayerScreenProvider.notifier,
)
.update(value);
}
},
items: DefaultPlayerScreen.values.map((screen) {
return DropdownMenuItem(
value: screen,
child: Text(screen.displayName),
);
}).toList(),
),
),
),
ListTile(
title: const Text('Lyrics Mode'),
subtitle: const Text(
'Choose how lyrics are displayed.',
),
trailing: DropdownButtonHideUnderline(
child: DropdownButton<LyricsMode>(
value: settings.lyricsMode,
onChanged: (LyricsMode? value) {
if (value != null) {
ref
.read(lyricsModeProvider.notifier)
.update(value);
}
},
items: LyricsMode.values.map((mode) {
return DropdownMenuItem(
value: mode,
child: Text(mode.displayName),
);
}).toList(),
),
),
),
const SizedBox(height: 8),
],
),
),
// Database Management Section
Card(
margin: EdgeInsets.zero,
@@ -324,7 +396,7 @@ class SettingsScreen extends ConsumerWidget {
fontSize: 18,
fontWeight: FontWeight.bold,
),
).padding(horizontal: 16, bottom: 8, top: 16),
).padding(horizontal: 16, top: 16),
const Text(
'Manage your music database and cached files.',
style: TextStyle(color: Colors.grey, fontSize: 14),