✨ Able to adjust lyrics mode and player default mode
This commit is contained in:
@@ -12,16 +12,38 @@ enum ImportMode {
|
|||||||
final String displayName;
|
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 {
|
class SettingsState {
|
||||||
final ImportMode importMode;
|
final ImportMode importMode;
|
||||||
final bool autoScan;
|
final bool autoScan;
|
||||||
final bool watchForChanges;
|
final bool watchForChanges;
|
||||||
|
final DefaultPlayerScreen defaultPlayerScreen;
|
||||||
|
final LyricsMode lyricsMode;
|
||||||
final Set<String> supportedFormats;
|
final Set<String> supportedFormats;
|
||||||
|
|
||||||
const SettingsState({
|
const SettingsState({
|
||||||
this.importMode = ImportMode.mixed,
|
this.importMode = ImportMode.mixed,
|
||||||
this.autoScan = true,
|
this.autoScan = true,
|
||||||
this.watchForChanges = true,
|
this.watchForChanges = true,
|
||||||
|
this.defaultPlayerScreen = DefaultPlayerScreen.cover,
|
||||||
|
this.lyricsMode = LyricsMode.auto,
|
||||||
this.supportedFormats = const {
|
this.supportedFormats = const {
|
||||||
'.mp3',
|
'.mp3',
|
||||||
'.flac',
|
'.flac',
|
||||||
@@ -38,12 +60,16 @@ class SettingsState {
|
|||||||
ImportMode? importMode,
|
ImportMode? importMode,
|
||||||
bool? autoScan,
|
bool? autoScan,
|
||||||
bool? watchForChanges,
|
bool? watchForChanges,
|
||||||
|
DefaultPlayerScreen? defaultPlayerScreen,
|
||||||
|
LyricsMode? lyricsMode,
|
||||||
Set<String>? supportedFormats,
|
Set<String>? supportedFormats,
|
||||||
}) {
|
}) {
|
||||||
return SettingsState(
|
return SettingsState(
|
||||||
importMode: importMode ?? this.importMode,
|
importMode: importMode ?? this.importMode,
|
||||||
autoScan: autoScan ?? this.autoScan,
|
autoScan: autoScan ?? this.autoScan,
|
||||||
watchForChanges: watchForChanges ?? this.watchForChanges,
|
watchForChanges: watchForChanges ?? this.watchForChanges,
|
||||||
|
defaultPlayerScreen: defaultPlayerScreen ?? this.defaultPlayerScreen,
|
||||||
|
lyricsMode: lyricsMode ?? this.lyricsMode,
|
||||||
supportedFormats: supportedFormats ?? this.supportedFormats,
|
supportedFormats: supportedFormats ?? this.supportedFormats,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -54,6 +80,8 @@ class SettingsNotifier extends _$SettingsNotifier {
|
|||||||
static const String _importModeKey = 'import_mode';
|
static const String _importModeKey = 'import_mode';
|
||||||
static const String _autoScanKey = 'auto_scan';
|
static const String _autoScanKey = 'auto_scan';
|
||||||
static const String _watchForChangesKey = 'watch_for_changes';
|
static const String _watchForChangesKey = 'watch_for_changes';
|
||||||
|
static const String _defaultPlayerScreenKey = 'default_player_screen';
|
||||||
|
static const String _lyricsModeKey = 'lyrics_mode';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SettingsState> build() async {
|
Future<SettingsState> build() async {
|
||||||
@@ -65,10 +93,20 @@ class SettingsNotifier extends _$SettingsNotifier {
|
|||||||
final autoScan = prefs.getBool(_autoScanKey) ?? true;
|
final autoScan = prefs.getBool(_autoScanKey) ?? true;
|
||||||
final watchForChanges = prefs.getBool(_watchForChangesKey) ?? 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(
|
return SettingsState(
|
||||||
importMode: importMode,
|
importMode: importMode,
|
||||||
autoScan: autoScan,
|
autoScan: autoScan,
|
||||||
watchForChanges: watchForChanges,
|
watchForChanges: watchForChanges,
|
||||||
|
defaultPlayerScreen: defaultPlayerScreen,
|
||||||
|
lyricsMode: lyricsMode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +136,26 @@ class SettingsNotifier extends _$SettingsNotifier {
|
|||||||
state = AsyncValue.data(state.value!.copyWith(watchForChanges: enabled));
|
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
|
// Convenience providers for specific settings
|
||||||
@@ -154,3 +212,39 @@ class WatchForChangesNotifier extends _$WatchForChangesNotifier {
|
|||||||
await ref.read(settingsProvider.notifier).setWatchForChanges(enabled);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class SettingsNotifierProvider
|
|||||||
SettingsNotifier create() => SettingsNotifier();
|
SettingsNotifier create() => SettingsNotifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$settingsNotifierHash() => r'6dc43c0f1d6ee7b7744dae2a8557b758574473d2';
|
String _$settingsNotifierHash() => r'4099dd1aa3dfc971c0761f314d196f3bc97315e7';
|
||||||
|
|
||||||
abstract class _$SettingsNotifier extends $AsyncNotifier<SettingsState> {
|
abstract class _$SettingsNotifier extends $AsyncNotifier<SettingsState> {
|
||||||
FutureOr<SettingsState> build();
|
FutureOr<SettingsState> build();
|
||||||
@@ -214,3 +214,113 @@ abstract class _$WatchForChangesNotifier extends $Notifier<bool> {
|
|||||||
element.handleValue(ref, created);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:ui';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:drift/drift.dart' as drift;
|
import 'package:drift/drift.dart' as drift;
|
||||||
import 'package:gap/gap.dart';
|
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/audio_provider.dart';
|
||||||
import 'package:groovybox/providers/db_provider.dart';
|
import 'package:groovybox/providers/db_provider.dart';
|
||||||
import 'package:groovybox/providers/lrc_fetcher_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/mini_player.dart';
|
||||||
import 'package:groovybox/ui/widgets/track_tile.dart';
|
import 'package:groovybox/ui/widgets/track_tile.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -35,7 +37,26 @@ class PlayerScreen extends HookConsumerWidget {
|
|||||||
final audioHandler = ref.watch(audioHandlerProvider);
|
final audioHandler = ref.watch(audioHandlerProvider);
|
||||||
final player = audioHandler.player;
|
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;
|
final isMobile = MediaQuery.sizeOf(context).width <= 800;
|
||||||
|
|
||||||
return StreamBuilder<Playlist>(
|
return StreamBuilder<Playlist>(
|
||||||
@@ -534,54 +555,42 @@ class _PlayerLyrics extends HookConsumerWidget {
|
|||||||
dynamic neteaseProviderInstance,
|
dynamic neteaseProviderInstance,
|
||||||
BuildContext context,
|
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) {
|
if (track.lyrics == null) {
|
||||||
// Show fetch lyrics UI
|
// Show fetch lyrics UI
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('No Lyrics Available'),
|
||||||
'No Lyrics Available',
|
|
||||||
style: TextStyle(fontStyle: FontStyle.italic, fontSize: 18),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
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)
|
if (lyricsFetcher.isLoading)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: LinearProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
)
|
||||||
if (lyricsFetcher.error != null)
|
else
|
||||||
Padding(
|
ElevatedButton.icon(
|
||||||
padding: const EdgeInsets.all(8.0),
|
icon: const Icon(Symbols.download),
|
||||||
child: Text(
|
label: const Text('Fetch Lyrics'),
|
||||||
lyricsFetcher.error!,
|
onPressed: () => _showFetchLyricsDialog(
|
||||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
context,
|
||||||
textAlign: TextAlign.center,
|
ref,
|
||||||
),
|
track,
|
||||||
),
|
track.path,
|
||||||
if (lyricsFetcher.successMessage != null)
|
metadataAsync.value,
|
||||||
Padding(
|
musixmatchProviderInstance,
|
||||||
padding: const EdgeInsets.all(8.0),
|
neteaseProviderInstance,
|
||||||
child: Text(
|
|
||||||
lyricsFetcher.successMessage!,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.green,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -599,24 +608,29 @@ class _PlayerLyrics extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Plain text lyrics
|
// Plain text lyrics
|
||||||
final isDesktop = MediaQuery.sizeOf(context).width > 800;
|
if (useCurvedStyle) {
|
||||||
if (isDesktop) {
|
|
||||||
return ListWheelScrollView.useDelegate(
|
return ListWheelScrollView.useDelegate(
|
||||||
itemExtent: 50,
|
itemExtent: 50,
|
||||||
perspective: 0.002,
|
perspective: 0.001,
|
||||||
offAxisFraction: 1.5,
|
offAxisFraction: isDesktop ? 1.5 : 0,
|
||||||
squeeze: 1.0,
|
squeeze: 1.0,
|
||||||
diameterRatio: 2,
|
diameterRatio: isDesktop
|
||||||
|
? 2
|
||||||
|
: RenderListWheelViewport.defaultDiameterRatio,
|
||||||
physics: const FixedExtentScrollPhysics(),
|
physics: const FixedExtentScrollPhysics(),
|
||||||
childDelegate: ListWheelChildBuilderDelegate(
|
childDelegate: ListWheelChildBuilderDelegate(
|
||||||
childCount: lyricsData.lines.length,
|
childCount: lyricsData.lines.length,
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
final line = lyricsData.lines[index];
|
final line = lyricsData.lines[index];
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: isDesktop
|
||||||
|
? Alignment.centerRight
|
||||||
|
: Alignment.center,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
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(
|
child: Container(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
@@ -1296,8 +1310,18 @@ class _TimedLyricsView extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Get lyrics mode setting
|
||||||
|
final lyricsMode = ref.watch(lyricsModeProvider);
|
||||||
final isDesktop = MediaQuery.sizeOf(context).width > 800;
|
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 listController = useMemoized(() => ListController(), []);
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final wheelScrollController = useMemoized(
|
final wheelScrollController = useMemoized(
|
||||||
@@ -1330,7 +1354,7 @@ class _TimedLyricsView extends HookConsumerWidget {
|
|||||||
if (currentIndex != previousIndex.value) {
|
if (currentIndex != previousIndex.value) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
previousIndex.value = currentIndex;
|
previousIndex.value = currentIndex;
|
||||||
if (isDesktop) {
|
if (useCurvedStyle) {
|
||||||
if (wheelScrollController.hasClients) {
|
if (wheelScrollController.hasClients) {
|
||||||
wheelScrollController.animateToItem(
|
wheelScrollController.animateToItem(
|
||||||
currentIndex,
|
currentIndex,
|
||||||
@@ -1352,14 +1376,16 @@ class _TimedLyricsView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final totalDurationMs = player.state.duration.inMilliseconds;
|
final totalDurationMs = player.state.duration.inMilliseconds;
|
||||||
|
|
||||||
if (isDesktop) {
|
if (useCurvedStyle) {
|
||||||
return ListWheelScrollView.useDelegate(
|
return ListWheelScrollView.useDelegate(
|
||||||
controller: wheelScrollController,
|
controller: wheelScrollController,
|
||||||
itemExtent: 50,
|
itemExtent: 50,
|
||||||
perspective: 0.002,
|
perspective: 0.001,
|
||||||
offAxisFraction: 1.5,
|
offAxisFraction: isDesktop ? 1.5 : 0,
|
||||||
squeeze: 1.0,
|
squeeze: 1.0,
|
||||||
diameterRatio: 2,
|
diameterRatio: isDesktop
|
||||||
|
? 2
|
||||||
|
: RenderListWheelViewport.defaultDiameterRatio,
|
||||||
physics: const FixedExtentScrollPhysics(),
|
physics: const FixedExtentScrollPhysics(),
|
||||||
childDelegate: ListWheelChildBuilderDelegate(
|
childDelegate: ListWheelChildBuilderDelegate(
|
||||||
childCount: lyrics.lines.length,
|
childCount: lyrics.lines.length,
|
||||||
@@ -1382,10 +1408,14 @@ class _TimedLyricsView extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: isDesktop
|
||||||
|
? Alignment.centerRight
|
||||||
|
: Alignment.center,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
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(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
).padding(horizontal: 16, bottom: 8, top: 16),
|
).padding(horizontal: 16, top: 16),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('Auto-scan music libraries'),
|
title: const Text('Auto-scan music libraries'),
|
||||||
subtitle: const Text(
|
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
|
// Database Management Section
|
||||||
Card(
|
Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@@ -324,7 +396,7 @@ class SettingsScreen extends ConsumerWidget {
|
|||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
).padding(horizontal: 16, bottom: 8, top: 16),
|
).padding(horizontal: 16, top: 16),
|
||||||
const Text(
|
const Text(
|
||||||
'Manage your music database and cached files.',
|
'Manage your music database and cached files.',
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
|||||||
Reference in New Issue
Block a user