Lyrics for remote tracks

This commit is contained in:
2025-12-20 01:01:12 +08:00
parent aaba0382cf
commit 7f67332590
8 changed files with 630 additions and 427 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_media_metadata/flutter_media_metadata.dart'; import 'package:flutter_media_metadata/flutter_media_metadata.dart';
import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/db.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/settings_provider.dart'; import 'package:groovybox/providers/settings_provider.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@@ -218,6 +219,15 @@ class TrackRepository extends _$TrackRepository {
await (db.update(db.tracks)..where((t) => t.id.equals(trackId))).write( await (db.update(db.tracks)..where((t) => t.id.equals(trackId))).write(
TracksCompanion(lyrics: Value(lyricsJson)), TracksCompanion(lyrics: Value(lyricsJson)),
); );
// Update current track provider if this is the current track
final currentTrackNotifier = ref.read(currentTrackProvider.notifier);
final currentTrack = currentTrackNotifier.state;
if (currentTrack != null && currentTrack.id == trackId) {
final updatedTrack = currentTrack.copyWith(lyrics: lyricsJson);
currentTrackNotifier.setTrack(updatedTrack);
debugPrint('Updated current track provider with imported lyrics');
}
} }
/// Get a single track by ID. /// Get a single track by ID.

View File

@@ -33,7 +33,7 @@ final class TrackRepositoryProvider
TrackRepository create() => TrackRepository(); TrackRepository create() => TrackRepository();
} }
String _$trackRepositoryHash() => r'655c231192698ef0c31920af846de47def7da81d'; String _$trackRepositoryHash() => r'606c68068cb2811a0982c950ba0f12d77cdf9d44';
abstract class _$TrackRepository extends $AsyncNotifier<void> { abstract class _$TrackRepository extends $AsyncNotifier<void> {
FutureOr<void> build(); FutureOr<void> build();

View File

@@ -3,7 +3,7 @@ import 'package:audio_service/audio_service.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:media_kit/media_kit.dart' as media_kit; import 'package:media_kit/media_kit.dart' as media_kit;
import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/db.dart' as db;
import 'package:groovybox/logic/metadata_service.dart'; 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/theme_provider.dart'; import 'package:groovybox/providers/theme_provider.dart';
@@ -51,18 +51,19 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
_container = container; _container = container;
} }
// Update theme color based on current track's album art and set current metadata // Update theme color based on current track's album art and set current metadata and track
void _updateThemeFromCurrentTrack(MediaItem mediaItem) async { void _updateThemeFromCurrentTrack(MediaItem mediaItem) async {
if (_container == null) return; if (_container == null) return;
try { try {
TrackMetadata? metadata; TrackMetadata? metadata;
db.Track? track;
// For remote tracks, get metadata from database // For remote tracks, get metadata from database
final urlResolver = _container!.read(remoteUrlResolverProvider); final urlResolver = _container!.read(remoteUrlResolverProvider);
if (urlResolver.isProtocolUrl(mediaItem.id)) { if (urlResolver.isProtocolUrl(mediaItem.id)) {
final database = _container!.read(databaseProvider); final database = _container!.read(databaseProvider);
final track = await (database.select( track = await (database.select(
database.tracks, database.tracks,
)..where((t) => t.path.equals(mediaItem.id))).getSingleOrNull(); )..where((t) => t.path.equals(mediaItem.id))).getSingleOrNull();
@@ -94,7 +95,13 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
seedColorNotifier.updateFromAlbumArtBytes(artBytes); seedColorNotifier.updateFromAlbumArtBytes(artBytes);
} }
} else { } else {
// For local tracks, use metadata service // For local tracks, get from database and use metadata service
final database = _container!.read(databaseProvider);
track = await (database.select(
database.tracks,
)..where((t) => t.path.equals(mediaItem.id))).getSingleOrNull();
// Use metadata service for local tracks
final metadataService = MetadataService(); final metadataService = MetadataService();
metadata = await metadataService.getMetadata(mediaItem.id); metadata = await metadataService.getMetadata(mediaItem.id);
@@ -103,18 +110,31 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
seedColorNotifier.updateFromAlbumArtBytes(metadata.artBytes); seedColorNotifier.updateFromAlbumArtBytes(metadata.artBytes);
} }
// Set current track
final trackNotifier = _container!.read(currentTrackProvider.notifier);
if (track != null) {
trackNotifier.setTrack(CurrentTrackData.fromTrack(track));
} else {
trackNotifier.clear();
}
// Set current track metadata // Set current track metadata
if (metadata != null) {
final metadataNotifier = _container!.read( final metadataNotifier = _container!.read(
currentTrackMetadataProvider.notifier, currentTrackMetadataProvider.notifier,
); );
if (metadata != null) {
metadataNotifier.setMetadata(metadata); metadataNotifier.setMetadata(metadata);
} else {
metadataNotifier.clear();
} }
} catch (e) { } catch (e) {
// If metadata retrieval fails, reset to default color and clear metadata // If metadata retrieval fails, reset to default color and clear metadata/track
final seedColorNotifier = _container!.read(seedColorProvider.notifier); final seedColorNotifier = _container!.read(seedColorProvider.notifier);
seedColorNotifier.resetToDefault(); seedColorNotifier.resetToDefault();
final trackNotifier = _container!.read(currentTrackProvider.notifier);
trackNotifier.clear();
final metadataNotifier = _container!.read( final metadataNotifier = _container!.read(
currentTrackMetadataProvider.notifier, currentTrackMetadataProvider.notifier,
); );
@@ -256,18 +276,18 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
} }
// New methods that accept Track objects with proper metadata // New methods that accept Track objects with proper metadata
Future<void> playTrack(Track track) async { Future<void> playTrack(db.Track track) async {
final mediaItem = _trackToMediaItem(track); final mediaItem = _trackToMediaItem(track);
await updateQueue([mediaItem]); await updateQueue([mediaItem]);
} }
Future<void> playTracks(List<Track> tracks, {int initialIndex = 0}) async { Future<void> playTracks(List<db.Track> tracks, {int initialIndex = 0}) async {
final mediaItems = tracks.map(_trackToMediaItem).toList(); final mediaItems = tracks.map(_trackToMediaItem).toList();
_queueIndex = initialIndex; _queueIndex = initialIndex;
await updateQueue(mediaItems); await updateQueue(mediaItems);
} }
MediaItem _trackToMediaItem(Track track) { MediaItem _trackToMediaItem(db.Track track) {
return MediaItem( return MediaItem(
id: track.path, id: track.path,
album: track.album, album: track.album,

View File

@@ -1,9 +1,63 @@
import 'package:groovybox/logic/audio_handler.dart'; import 'package:groovybox/logic/audio_handler.dart';
import 'package:groovybox/logic/metadata_service.dart'; import 'package:groovybox/logic/metadata_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:groovybox/data/db.dart' as db;
part 'audio_provider.g.dart'; part 'audio_provider.g.dart';
// Simple data class for current track to avoid drift type issues
class CurrentTrackData {
final int id;
final String title;
final String? artist;
final String? album;
final String path;
final String? lyrics;
final int lyricsOffset;
CurrentTrackData({
required this.id,
required this.title,
this.artist,
this.album,
required this.path,
this.lyrics,
required this.lyricsOffset,
});
factory CurrentTrackData.fromTrack(db.Track track) {
return CurrentTrackData(
id: track.id,
title: track.title,
artist: track.artist,
album: track.album,
path: track.path,
lyrics: track.lyrics,
lyricsOffset: track.lyricsOffset,
);
}
CurrentTrackData copyWith({
int? id,
String? title,
String? artist,
String? album,
String? path,
String? lyrics,
int? lyricsOffset,
}) {
return CurrentTrackData(
id: id ?? this.id,
title: title ?? this.title,
artist: artist ?? this.artist,
album: album ?? this.album,
path: path ?? this.path,
lyrics: lyrics ?? this.lyrics,
lyricsOffset: lyricsOffset ?? this.lyricsOffset,
);
}
}
// This should be set after AudioService.init in main.dart // This should be set after AudioService.init in main.dart
late AudioHandler _audioHandler; late AudioHandler _audioHandler;
@@ -17,6 +71,22 @@ void setAudioHandler(AudioHandler handler) {
_audioHandler = handler; _audioHandler = handler;
} }
@Riverpod(keepAlive: true)
class CurrentTrackNotifier extends _$CurrentTrackNotifier {
@override
CurrentTrackData? build() {
return null;
}
void setTrack(CurrentTrackData? track) {
state = track;
}
void clear() {
state = null;
}
}
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class CurrentTrackMetadataNotifier extends _$CurrentTrackMetadataNotifier { class CurrentTrackMetadataNotifier extends _$CurrentTrackMetadataNotifier {
@override @override

View File

@@ -50,6 +50,60 @@ final class AudioHandlerProvider
String _$audioHandlerHash() => r'65fbd92e049fe4f3a0763516f1e68e1614f7630f'; String _$audioHandlerHash() => r'65fbd92e049fe4f3a0763516f1e68e1614f7630f';
@ProviderFor(CurrentTrackNotifier)
const currentTrackProvider = CurrentTrackNotifierProvider._();
final class CurrentTrackNotifierProvider
extends $NotifierProvider<CurrentTrackNotifier, CurrentTrackData?> {
const CurrentTrackNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentTrackProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentTrackNotifierHash();
@$internal
@override
CurrentTrackNotifier create() => CurrentTrackNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(CurrentTrackData? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<CurrentTrackData?>(value),
);
}
}
String _$currentTrackNotifierHash() =>
r'faa718574ece8c3c4b8f19b70d79d142b4b7f3e9';
abstract class _$CurrentTrackNotifier extends $Notifier<CurrentTrackData?> {
CurrentTrackData? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<CurrentTrackData?, CurrentTrackData?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<CurrentTrackData?, CurrentTrackData?>,
CurrentTrackData?,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(CurrentTrackMetadataNotifier) @ProviderFor(CurrentTrackMetadataNotifier)
const currentTrackMetadataProvider = CurrentTrackMetadataNotifierProvider._(); const currentTrackMetadataProvider = CurrentTrackMetadataNotifierProvider._();

View File

@@ -3,6 +3,7 @@ import 'package:groovybox/data/db.dart' as db;
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:groovybox/logic/lrc_providers.dart'; import 'package:groovybox/logic/lrc_providers.dart';
import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/lyrics_parser.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/ui/screens/player_screen.dart'; import 'package:groovybox/ui/screens/player_screen.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -60,6 +61,16 @@ class LyricsFetcher extends _$LyricsFetcher {
debugPrint('Updated database with lyrics for track $trackId'); debugPrint('Updated database with lyrics for track $trackId');
// Update the current track provider if this is the current track
final currentTrackNotifier = ref.read(currentTrackProvider.notifier);
final currentTrack = currentTrackNotifier.state;
if (currentTrack != null && currentTrack.id == trackId) {
// Update the current track with new lyrics
final updatedTrack = currentTrack.copyWith(lyrics: lyricsJson);
currentTrackNotifier.setTrack(updatedTrack);
debugPrint('Updated current track provider with new lyrics');
}
// Invalidate the track provider to refresh the UI // Invalidate the track provider to refresh the UI
ref.invalidate(trackByPathProvider(trackPath)); ref.invalidate(trackByPathProvider(trackPath));

View File

@@ -247,7 +247,7 @@ class RemoteUrlResolver {
try { try {
// Create Jellyfin client and authenticate // Create Jellyfin client and authenticate
final client = JellyfinDart(basePathOverride: provider.serverUrl); final client = JellyfinDart(basePathOverride: provider.serverUrl);
client.setDeviceId('groovybox-${providerId}'); client.setDeviceId('groovybox-$providerId');
client.setVersion('1.0.0'); client.setVersion('1.0.0');
final userApi = client.getUserApi(); final userApi = client.getUserApi();

View File

@@ -182,7 +182,6 @@ class _MobileLayout extends HookConsumerWidget {
).padding(bottom: MediaQuery.paddingOf(context).bottom), ).padding(bottom: MediaQuery.paddingOf(context).bottom),
ViewMode.lyrics => _LyricsView( ViewMode.lyrics => _LyricsView(
key: const ValueKey('lyrics'), key: const ValueKey('lyrics'),
trackPath: trackPath,
player: player, player: player,
), ),
ViewMode.queue => _QueueView( ViewMode.queue => _QueueView(
@@ -301,7 +300,7 @@ class _DesktopLayout extends HookConsumerWidget {
width: MediaQuery.sizeOf(context).width * 0.6, width: MediaQuery.sizeOf(context).width * 0.6,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: _PlayerLyrics(trackPath: trackPath, player: player), child: _PlayerLyrics(player: player),
), ),
), ),
], ],
@@ -384,10 +383,9 @@ class _CoverView extends StatelessWidget {
} }
class _LyricsView extends StatelessWidget { class _LyricsView extends StatelessWidget {
final String trackPath;
final Player player; final Player player;
const _LyricsView({super.key, required this.trackPath, required this.player}); const _LyricsView({super.key, required this.player});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -396,7 +394,7 @@ class _LyricsView extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: _PlayerLyrics(trackPath: trackPath, player: player), child: _PlayerLyrics(player: player),
), ),
), ),
MiniPlayer(enableTapToOpen: false), MiniPlayer(enableTapToOpen: false),
@@ -456,17 +454,14 @@ class _PlayerCoverArt extends StatelessWidget {
} }
class _PlayerLyrics extends HookConsumerWidget { class _PlayerLyrics extends HookConsumerWidget {
final String? trackPath;
final Player player; final Player player;
const _PlayerLyrics({this.trackPath, required this.player}); const _PlayerLyrics({required this.player});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
// Watch for track data (including lyrics) by path // Watch for current track data (including lyrics)
final trackAsync = trackPath != null final currentTrack = ref.watch(currentTrackProvider);
? ref.watch(trackByPathProvider(trackPath!))
: const AsyncValue<db.Track?>.data(null);
// For now, skip metadata loading to avoid provider issues // For now, skip metadata loading to avoid provider issues
final AsyncValue<TrackMetadata> metadataAsync = AsyncValue.data( final AsyncValue<TrackMetadata> metadataAsync = AsyncValue.data(
@@ -477,11 +472,68 @@ class _PlayerLyrics extends HookConsumerWidget {
final musixmatchProviderInstance = ref.watch(musixmatchProvider); final musixmatchProviderInstance = ref.watch(musixmatchProvider);
final neteaseProviderInstance = ref.watch(neteaseProvider); final neteaseProviderInstance = ref.watch(neteaseProvider);
return trackAsync.when( // Simulate async behavior for compatibility
loading: () => const Center(child: CircularProgressIndicator()), if (currentTrack == null) {
error: (e, _) => Center(child: Text('Error: $e')), return const Center(child: CircularProgressIndicator());
data: (track) { }
if (track == null || track.lyrics == null) {
// Convert CurrentTrackData to db.Track for compatibility
final track = db.Track(
id: currentTrack.id,
title: currentTrack.title,
artist: currentTrack.artist,
album: currentTrack.album,
path: currentTrack.path,
lyrics: currentTrack.lyrics,
lyricsOffset: currentTrack.lyricsOffset,
duration: null,
artUri: null,
addedAt: DateTime.now(),
);
return _buildLyricsContent(
track,
metadataAsync,
ref,
lyricsFetcher,
musixmatchProviderInstance,
neteaseProviderInstance,
context,
);
}
void _showFetchLyricsDialog(
BuildContext context,
WidgetRef ref,
db.Track track,
String trackPath,
dynamic metadataObj,
musixmatchProvider,
neteaseProvider,
) {
showDialog(
context: context,
builder: (context) => _FetchLyricsDialog(
track: track,
trackPath: trackPath,
metadataObj: metadataObj,
ref: ref,
musixmatchProvider: musixmatchProvider,
neteaseProvider: neteaseProvider,
),
);
}
Widget _buildLyricsContent(
db.Track track,
AsyncValue<TrackMetadata> metadataAsync,
WidgetRef ref,
dynamic lyricsFetcher,
dynamic musixmatchProviderInstance,
dynamic neteaseProviderInstance,
BuildContext context,
) {
if (track.lyrics == null) {
// Show fetch lyrics UI // Show fetch lyrics UI
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -494,17 +546,15 @@ class _PlayerLyrics extends HookConsumerWidget {
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.download), icon: const Icon(Icons.download),
label: const Text('Fetch Lyrics'), label: const Text('Fetch Lyrics'),
onPressed: track != null && trackPath != null onPressed: () => _showFetchLyricsDialog(
? () => _showFetchLyricsDialog(
context, context,
ref, ref,
track, track,
trackPath!, track.path,
metadataAsync.value, metadataAsync.value,
musixmatchProviderInstance, musixmatchProviderInstance,
neteaseProviderInstance, neteaseProviderInstance,
) ),
: null,
), ),
if (lyricsFetcher.isLoading) if (lyricsFetcher.isLoading)
Padding( Padding(
@@ -544,7 +594,7 @@ class _PlayerLyrics extends HookConsumerWidget {
return _TimedLyricsView( return _TimedLyricsView(
lyrics: lyricsData, lyrics: lyricsData,
player: player, player: player,
trackPath: trackPath!, track: track,
); );
} else { } else {
// Plain text lyrics // Plain text lyrics
@@ -603,30 +653,6 @@ class _PlayerLyrics extends HookConsumerWidget {
} catch (e) { } catch (e) {
return Center(child: Text('Error parsing lyrics: $e')); return Center(child: Text('Error parsing lyrics: $e'));
} }
},
);
}
void _showFetchLyricsDialog(
BuildContext context,
WidgetRef ref,
db.Track track,
String trackPath,
dynamic metadataObj,
musixmatchProvider,
neteaseProvider,
) {
showDialog(
context: context,
builder: (context) => _FetchLyricsDialog(
track: track,
trackPath: trackPath,
metadataObj: metadataObj,
ref: ref,
musixmatchProvider: musixmatchProvider,
neteaseProvider: neteaseProvider,
),
);
} }
} }
@@ -779,14 +805,14 @@ class _FetchLyricsDialog extends StatelessWidget {
} }
class _LyricsAdjustButton extends HookConsumerWidget { class _LyricsAdjustButton extends HookConsumerWidget {
final String trackPath;
final Player player; final Player player;
const _LyricsAdjustButton({required this.trackPath, required this.player}); const _LyricsAdjustButton({required this.player});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final trackAsync = ref.watch(trackByPathProvider(trackPath)); final currentTrack = ref.watch(currentTrackProvider);
// For now, skip metadata loading to avoid provider issues // For now, skip metadata loading to avoid provider issues
final AsyncValue<TrackMetadata> metadataAsync = AsyncValue.data( final AsyncValue<TrackMetadata> metadataAsync = AsyncValue.data(
TrackMetadata(), TrackMetadata(),
@@ -794,6 +820,11 @@ class _LyricsAdjustButton extends HookConsumerWidget {
final musixmatchProviderInstance = ref.watch(musixmatchProvider); final musixmatchProviderInstance = ref.watch(musixmatchProvider);
final neteaseProviderInstance = ref.watch(neteaseProvider); final neteaseProviderInstance = ref.watch(neteaseProvider);
// Don't show the button if there's no current track
if (currentTrack == null) {
return const SizedBox.shrink();
}
return IconButton( return IconButton(
icon: const Icon(Icons.settings_applications), icon: const Icon(Icons.settings_applications),
iconSize: 24, iconSize: 24,
@@ -801,7 +832,7 @@ class _LyricsAdjustButton extends HookConsumerWidget {
onPressed: () => _showLyricsRefreshDialog( onPressed: () => _showLyricsRefreshDialog(
context, context,
ref, ref,
trackAsync, currentTrack,
metadataAsync, metadataAsync,
musixmatchProviderInstance, musixmatchProviderInstance,
neteaseProviderInstance, neteaseProviderInstance,
@@ -835,11 +866,25 @@ class _LyricsAdjustButton extends HookConsumerWidget {
void _showLyricsRefreshDialog( void _showLyricsRefreshDialog(
BuildContext context, BuildContext context,
WidgetRef ref, WidgetRef ref,
AsyncValue<db.Track?> trackAsync, CurrentTrackData currentTrack,
AsyncValue<TrackMetadata> metadataAsync, AsyncValue<TrackMetadata> metadataAsync,
musixmatchProvider, musixmatchProvider,
neteaseProvider, neteaseProvider,
) { ) {
// Convert CurrentTrackData to db.Track for compatibility
final track = db.Track(
id: currentTrack.id,
title: currentTrack.title,
artist: currentTrack.artist,
album: currentTrack.album,
path: currentTrack.path,
lyrics: currentTrack.lyrics,
lyricsOffset: currentTrack.lyricsOffset,
duration: null,
artUri: null,
addedAt: DateTime.now(),
);
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@@ -858,24 +903,19 @@ class _LyricsAdjustButton extends HookConsumerWidget {
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: const Text('Re-fetch'), label: const Text('Re-fetch'),
onPressed: trackAsync.maybeWhen( onPressed: () {
data: (track) => track != null
? () {
Navigator.of(context).pop(); Navigator.of(context).pop();
final metadata = metadataAsync.value; final metadata = metadataAsync.value;
_showFetchLyricsDialog( _showFetchLyricsDialog(
context, context,
ref, ref,
track, track,
trackPath, currentTrack.path,
metadata, metadata,
musixmatchProvider, musixmatchProvider,
neteaseProvider, neteaseProvider,
); );
} },
: null,
orElse: () => null,
),
), ),
), ),
Expanded( Expanded(
@@ -886,13 +926,9 @@ class _LyricsAdjustButton extends HookConsumerWidget {
backgroundColor: Colors.red, backgroundColor: Colors.red,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
onPressed: trackAsync.maybeWhen( onPressed: () async {
data: (track) => track != null
? () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
debugPrint( debugPrint('Clearing lyrics for track ${track.id}');
'Clearing lyrics for track ${track.id}',
);
final database = ref.read(databaseProvider); final database = ref.read(databaseProvider);
await (database.update( await (database.update(
database.tracks, database.tracks,
@@ -902,17 +938,33 @@ class _LyricsAdjustButton extends HookConsumerWidget {
), ),
); );
debugPrint('Cleared lyrics from database'); debugPrint('Cleared lyrics from database');
// Invalidate the track provider to refresh the UI
ref.invalidate( // Update current track provider if this is the current track
trackByPathProvider(trackPath), final currentTrackNotifier = ref.read(
currentTrackProvider.notifier,
); );
final currentTrackState = ref.watch(
currentTrackProvider,
);
if (currentTrackState != null &&
currentTrackState.id == track.id) {
final updatedTrack = currentTrackState.copyWith(
lyrics: null,
);
currentTrackNotifier.setTrack(updatedTrack);
debugPrint( debugPrint(
'Invalidated track provider for $trackPath', 'Updated current track provider - cleared lyrics',
); );
} }
: null,
orElse: () => null, // Invalidate the track provider to refresh the UI
), ref.invalidate(
trackByPathProvider(currentTrack.path),
);
debugPrint(
'Invalidated track provider for ${currentTrack.path}',
);
},
), ),
), ),
], ],
@@ -922,21 +974,16 @@ class _LyricsAdjustButton extends HookConsumerWidget {
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.sync), icon: const Icon(Icons.sync),
label: const Text('Live Sync Lyrics'), label: const Text('Live Sync Lyrics'),
onPressed: trackAsync.maybeWhen( onPressed: () {
data: (track) => track != null
? () {
Navigator.of(context).pop(); Navigator.of(context).pop();
_showLiveLyricsSyncDialog( _showLiveLyricsSyncDialog(
context, context,
ref, ref,
track, track,
trackPath, currentTrack.path,
player, player,
); );
} },
: null,
orElse: () => null,
),
), ),
), ),
SizedBox( SizedBox(
@@ -944,20 +991,15 @@ class _LyricsAdjustButton extends HookConsumerWidget {
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.tune), icon: const Icon(Icons.tune),
label: const Text('Manual Offset'), label: const Text('Manual Offset'),
onPressed: trackAsync.maybeWhen( onPressed: () {
data: (track) => track != null
? () {
Navigator.of(context).pop(); Navigator.of(context).pop();
_showLyricsOffsetDialog( _showLyricsOffsetDialog(
context, context,
ref, ref,
track, track,
trackPath, currentTrack.path,
); );
} },
: null,
orElse: () => null,
),
), ),
), ),
], ],
@@ -1243,12 +1285,12 @@ class _QueueView extends HookConsumerWidget {
class _TimedLyricsView extends HookConsumerWidget { class _TimedLyricsView extends HookConsumerWidget {
final LyricsData lyrics; final LyricsData lyrics;
final Player player; final Player player;
final String trackPath; final db.Track track;
const _TimedLyricsView({ const _TimedLyricsView({
required this.lyrics, required this.lyrics,
required this.player, required this.player,
required this.trackPath, required this.track,
}); });
@override @override
@@ -1263,14 +1305,8 @@ class _TimedLyricsView extends HookConsumerWidget {
); );
final previousIndex = useState(-1); final previousIndex = useState(-1);
// Get track data to access lyrics offset // Use track directly to access lyrics offset
final trackAsync = ref.watch(trackByPathProvider(trackPath)); final lyricsOffset = track.lyricsOffset;
return trackAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (track) {
final lyricsOffset = track?.lyricsOffset ?? 0;
return StreamBuilder<Duration>( return StreamBuilder<Duration>(
stream: player.stream.position, stream: player.stream.position,
@@ -1369,28 +1405,19 @@ class _TimedLyricsView extends HookConsumerWidget {
: FontWeight.normal, : FontWeight.normal,
color: isActive color: isActive
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context) : Theme.of(context).colorScheme.onSurface
.colorScheme
.onSurface
.withValues(alpha: 0.7), .withValues(alpha: 0.7),
), ),
textAlign: TextAlign.left, textAlign: TextAlign.left,
child: () { child: () {
final displayText = line.text; final displayText = line.text;
return isActive && return isActive && progress > 0.0 && progress < 1.0
progress > 0.0 &&
progress < 1.0
? ShaderMask( ? ShaderMask(
shaderCallback: (bounds) => shaderCallback: (bounds) => LinearGradient(
LinearGradient(
colors: [ colors: [
Theme.of( Theme.of(context).colorScheme.primary,
context, Theme.of(context).colorScheme.onSurface
).colorScheme.primary,
Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7), .withValues(alpha: 0.7),
], ],
stops: [progress, progress], stops: [progress, progress],
@@ -1437,8 +1464,7 @@ class _TimedLyricsView extends HookConsumerWidget {
? (lyrics.lines[index + 1].timeMs ?? startTime) ? (lyrics.lines[index + 1].timeMs ?? startTime)
: totalDurationMs; : totalDurationMs;
if (endTime > startTime) { if (endTime > startTime) {
progress = progress = ((positionMs - startTime) / (endTime - startTime))
((positionMs - startTime) / (endTime - startTime))
.clamp(0.0, 1.0); .clamp(0.0, 1.0);
} }
} }
@@ -1458,9 +1484,7 @@ class _TimedLyricsView extends HookConsumerWidget {
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
style: Theme.of(context).textTheme.bodyLarge!.copyWith( style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: isActive ? 20 : 16, fontSize: isActive ? 20 : 16,
fontWeight: isActive fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
? FontWeight.bold
: FontWeight.normal,
color: isActive color: isActive
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of( : Theme.of(
@@ -1476,8 +1500,9 @@ class _TimedLyricsView extends HookConsumerWidget {
shaderCallback: (bounds) => LinearGradient( shaderCallback: (bounds) => LinearGradient(
colors: [ colors: [
Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.onSurface Theme.of(
.withValues(alpha: 0.7), context,
).colorScheme.onSurface.withValues(alpha: 0.7),
], ],
stops: [progress, progress], stops: [progress, progress],
).createShader(bounds), ).createShader(bounds),
@@ -1492,8 +1517,6 @@ class _TimedLyricsView extends HookConsumerWidget {
); );
}, },
); );
},
);
} }
} }
@@ -1711,7 +1734,7 @@ class _PlayerControls extends HookWidget {
child: Row( child: Row(
spacing: 16, spacing: 16,
children: [ children: [
_LyricsAdjustButton(trackPath: trackPath, player: player), _LyricsAdjustButton(player: player),
Expanded( Expanded(
child: StreamBuilder<double>( child: StreamBuilder<double>(
stream: player.stream.volume, stream: player.stream.volume,
@@ -1807,6 +1830,21 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget {
db.TracksCompanion(lyricsOffset: drift.Value(tempOffset.value)), db.TracksCompanion(lyricsOffset: drift.Value(tempOffset.value)),
); );
// Update current track provider if this is the current track
final currentTrackNotifier = ref.read(
currentTrackProvider.notifier,
);
final currentTrack = ref.watch(currentTrackProvider);
if (currentTrack != null && currentTrack.id == track.id) {
final updatedTrack = currentTrack.copyWith(
lyricsOffset: tempOffset.value,
);
currentTrackNotifier.setTrack(updatedTrack);
debugPrint(
'Updated current track provider with new lyrics offset',
);
}
// Invalidate the track provider to refresh the UI // Invalidate the track provider to refresh the UI
ref.invalidate(trackByPathProvider(trackPath)); ref.invalidate(trackByPathProvider(trackPath));