diff --git a/lib/data/track_repository.dart b/lib/data/track_repository.dart index 2635354..06589ff 100644 --- a/lib/data/track_repository.dart +++ b/lib/data/track_repository.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_media_metadata/flutter_media_metadata.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/settings_provider.dart'; 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( 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. diff --git a/lib/data/track_repository.g.dart b/lib/data/track_repository.g.dart index 5570e4b..7c0c6b6 100644 --- a/lib/data/track_repository.g.dart +++ b/lib/data/track_repository.g.dart @@ -33,7 +33,7 @@ final class TrackRepositoryProvider TrackRepository create() => TrackRepository(); } -String _$trackRepositoryHash() => r'655c231192698ef0c31920af846de47def7da81d'; +String _$trackRepositoryHash() => r'606c68068cb2811a0982c950ba0f12d77cdf9d44'; abstract class _$TrackRepository extends $AsyncNotifier { FutureOr build(); diff --git a/lib/logic/audio_handler.dart b/lib/logic/audio_handler.dart index 20687dc..ad3bf97 100644 --- a/lib/logic/audio_handler.dart +++ b/lib/logic/audio_handler.dart @@ -3,7 +3,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart' as http; 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/providers/audio_provider.dart'; import 'package:groovybox/providers/theme_provider.dart'; @@ -51,18 +51,19 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { _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 { if (_container == null) return; try { TrackMetadata? metadata; + db.Track? track; // For remote tracks, get metadata from database final urlResolver = _container!.read(remoteUrlResolverProvider); if (urlResolver.isProtocolUrl(mediaItem.id)) { final database = _container!.read(databaseProvider); - final track = await (database.select( + track = await (database.select( database.tracks, )..where((t) => t.path.equals(mediaItem.id))).getSingleOrNull(); @@ -94,7 +95,13 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { seedColorNotifier.updateFromAlbumArtBytes(artBytes); } } 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(); metadata = await metadataService.getMetadata(mediaItem.id); @@ -103,18 +110,31 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { 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 + final metadataNotifier = _container!.read( + currentTrackMetadataProvider.notifier, + ); if (metadata != null) { - final metadataNotifier = _container!.read( - currentTrackMetadataProvider.notifier, - ); metadataNotifier.setMetadata(metadata); + } else { + metadataNotifier.clear(); } } 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); seedColorNotifier.resetToDefault(); + final trackNotifier = _container!.read(currentTrackProvider.notifier); + trackNotifier.clear(); + final metadataNotifier = _container!.read( currentTrackMetadataProvider.notifier, ); @@ -256,18 +276,18 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { } // New methods that accept Track objects with proper metadata - Future playTrack(Track track) async { + Future playTrack(db.Track track) async { final mediaItem = _trackToMediaItem(track); await updateQueue([mediaItem]); } - Future playTracks(List tracks, {int initialIndex = 0}) async { + Future playTracks(List tracks, {int initialIndex = 0}) async { final mediaItems = tracks.map(_trackToMediaItem).toList(); _queueIndex = initialIndex; await updateQueue(mediaItems); } - MediaItem _trackToMediaItem(Track track) { + MediaItem _trackToMediaItem(db.Track track) { return MediaItem( id: track.path, album: track.album, diff --git a/lib/providers/audio_provider.dart b/lib/providers/audio_provider.dart index bc80db2..26cc5db 100644 --- a/lib/providers/audio_provider.dart +++ b/lib/providers/audio_provider.dart @@ -1,9 +1,63 @@ import 'package:groovybox/logic/audio_handler.dart'; import 'package:groovybox/logic/metadata_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:groovybox/data/db.dart' as db; 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 late AudioHandler _audioHandler; @@ -17,6 +71,22 @@ void setAudioHandler(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) class CurrentTrackMetadataNotifier extends _$CurrentTrackMetadataNotifier { @override diff --git a/lib/providers/audio_provider.g.dart b/lib/providers/audio_provider.g.dart index ee449e7..b6f2a22 100644 --- a/lib/providers/audio_provider.g.dart +++ b/lib/providers/audio_provider.g.dart @@ -50,6 +50,60 @@ final class AudioHandlerProvider String _$audioHandlerHash() => r'65fbd92e049fe4f3a0763516f1e68e1614f7630f'; +@ProviderFor(CurrentTrackNotifier) +const currentTrackProvider = CurrentTrackNotifierProvider._(); + +final class CurrentTrackNotifierProvider + extends $NotifierProvider { + 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(value), + ); + } +} + +String _$currentTrackNotifierHash() => + r'faa718574ece8c3c4b8f19b70d79d142b4b7f3e9'; + +abstract class _$CurrentTrackNotifier extends $Notifier { + CurrentTrackData? build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + CurrentTrackData?, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + @ProviderFor(CurrentTrackMetadataNotifier) const currentTrackMetadataProvider = CurrentTrackMetadataNotifierProvider._(); diff --git a/lib/providers/lrc_fetcher_provider.dart b/lib/providers/lrc_fetcher_provider.dart index 386617a..3ed8812 100644 --- a/lib/providers/lrc_fetcher_provider.dart +++ b/lib/providers/lrc_fetcher_provider.dart @@ -3,6 +3,7 @@ import 'package:groovybox/data/db.dart' as db; import 'package:drift/drift.dart' as drift; import 'package:groovybox/logic/lrc_providers.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/ui/screens/player_screen.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -60,6 +61,16 @@ class LyricsFetcher extends _$LyricsFetcher { 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 ref.invalidate(trackByPathProvider(trackPath)); diff --git a/lib/providers/remote_provider.dart b/lib/providers/remote_provider.dart index b6616c8..95961f8 100644 --- a/lib/providers/remote_provider.dart +++ b/lib/providers/remote_provider.dart @@ -247,7 +247,7 @@ class RemoteUrlResolver { try { // Create Jellyfin client and authenticate final client = JellyfinDart(basePathOverride: provider.serverUrl); - client.setDeviceId('groovybox-${providerId}'); + client.setDeviceId('groovybox-$providerId'); client.setVersion('1.0.0'); final userApi = client.getUserApi(); diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index 4ad15d7..1195e11 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -182,7 +182,6 @@ class _MobileLayout extends HookConsumerWidget { ).padding(bottom: MediaQuery.paddingOf(context).bottom), ViewMode.lyrics => _LyricsView( key: const ValueKey('lyrics'), - trackPath: trackPath, player: player, ), ViewMode.queue => _QueueView( @@ -301,7 +300,7 @@ class _DesktopLayout extends HookConsumerWidget { width: MediaQuery.sizeOf(context).width * 0.6, child: Padding( 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 { - final String trackPath; final Player player; - const _LyricsView({super.key, required this.trackPath, required this.player}); + const _LyricsView({super.key, required this.player}); @override Widget build(BuildContext context) { @@ -396,7 +394,7 @@ class _LyricsView extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.all(16.0), - child: _PlayerLyrics(trackPath: trackPath, player: player), + child: _PlayerLyrics(player: player), ), ), MiniPlayer(enableTapToOpen: false), @@ -456,17 +454,14 @@ class _PlayerCoverArt extends StatelessWidget { } class _PlayerLyrics extends HookConsumerWidget { - final String? trackPath; final Player player; - const _PlayerLyrics({this.trackPath, required this.player}); + const _PlayerLyrics({required this.player}); @override Widget build(BuildContext context, WidgetRef ref) { - // Watch for track data (including lyrics) by path - final trackAsync = trackPath != null - ? ref.watch(trackByPathProvider(trackPath!)) - : const AsyncValue.data(null); + // Watch for current track data (including lyrics) + final currentTrack = ref.watch(currentTrackProvider); // For now, skip metadata loading to avoid provider issues final AsyncValue metadataAsync = AsyncValue.data( @@ -477,133 +472,33 @@ class _PlayerLyrics extends HookConsumerWidget { final musixmatchProviderInstance = ref.watch(musixmatchProvider); final neteaseProviderInstance = ref.watch(neteaseProvider); - return trackAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (e, _) => Center(child: Text('Error: $e')), - data: (track) { - if (track == null || track.lyrics == null) { - // Show fetch lyrics UI - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'No Lyrics Available', - style: TextStyle(fontStyle: FontStyle.italic, fontSize: 18), - ), - const SizedBox(height: 16), - ElevatedButton.icon( - icon: const Icon(Icons.download), - label: const Text('Fetch Lyrics'), - onPressed: track != null && trackPath != null - ? () => _showFetchLyricsDialog( - context, - ref, - track, - trackPath!, - metadataAsync.value, - musixmatchProviderInstance, - neteaseProviderInstance, - ) - : null, - ), - 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, - ), - ), - ], - ); - } + // Simulate async behavior for compatibility + if (currentTrack == null) { + return const Center(child: CircularProgressIndicator()); + } - try { - final lyricsData = LyricsData.fromJsonString(track.lyrics!); + // 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(), + ); - if (lyricsData.type == 'timed') { - return _TimedLyricsView( - lyrics: lyricsData, - player: player, - trackPath: trackPath!, - ); - } else { - // Plain text lyrics - final isDesktop = MediaQuery.sizeOf(context).width > 800; - if (isDesktop) { - return ListWheelScrollView.useDelegate( - itemExtent: 50, - perspective: 0.002, - offAxisFraction: 1.5, - squeeze: 1.0, - diameterRatio: 2, - physics: const FixedExtentScrollPhysics(), - childDelegate: ListWheelChildBuilderDelegate( - childCount: lyricsData.lines.length, - builder: (context, index) { - final line = lyricsData.lines[index]; - return Align( - alignment: Alignment.centerRight, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: MediaQuery.sizeOf(context).width * 0.4, - ), - child: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Text( - line.text, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.left, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ); - }, - ), - ); - } else { - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: lyricsData.lines.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - lyricsData.lines[index].text, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - ); - }, - ); - } - } - } catch (e) { - return Center(child: Text('Error parsing lyrics: $e')); - } - }, + return _buildLyricsContent( + track, + metadataAsync, + ref, + lyricsFetcher, + musixmatchProviderInstance, + neteaseProviderInstance, + context, ); } @@ -628,6 +523,137 @@ class _PlayerLyrics extends HookConsumerWidget { ), ); } + + Widget _buildLyricsContent( + db.Track track, + AsyncValue metadataAsync, + WidgetRef ref, + dynamic lyricsFetcher, + dynamic musixmatchProviderInstance, + dynamic neteaseProviderInstance, + BuildContext context, + ) { + 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), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + icon: const Icon(Icons.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, + ), + ), + ], + ); + } + + try { + final lyricsData = LyricsData.fromJsonString(track.lyrics!); + + if (lyricsData.type == 'timed') { + return _TimedLyricsView( + lyrics: lyricsData, + player: player, + track: track, + ); + } else { + // Plain text lyrics + final isDesktop = MediaQuery.sizeOf(context).width > 800; + if (isDesktop) { + return ListWheelScrollView.useDelegate( + itemExtent: 50, + perspective: 0.002, + offAxisFraction: 1.5, + squeeze: 1.0, + diameterRatio: 2, + physics: const FixedExtentScrollPhysics(), + childDelegate: ListWheelChildBuilderDelegate( + childCount: lyricsData.lines.length, + builder: (context, index) { + final line = lyricsData.lines[index]; + return Align( + alignment: Alignment.centerRight, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.4, + ), + child: Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + line.text, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.left, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + }, + ), + ); + } else { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: lyricsData.lines.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + lyricsData.lines[index].text, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ); + }, + ); + } + } + } catch (e) { + return Center(child: Text('Error parsing lyrics: $e')); + } + } } class _FetchLyricsDialog extends StatelessWidget { @@ -779,14 +805,14 @@ class _FetchLyricsDialog extends StatelessWidget { } class _LyricsAdjustButton extends HookConsumerWidget { - final String trackPath; final Player player; - const _LyricsAdjustButton({required this.trackPath, required this.player}); + const _LyricsAdjustButton({required this.player}); @override 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 final AsyncValue metadataAsync = AsyncValue.data( TrackMetadata(), @@ -794,6 +820,11 @@ class _LyricsAdjustButton extends HookConsumerWidget { final musixmatchProviderInstance = ref.watch(musixmatchProvider); 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( icon: const Icon(Icons.settings_applications), iconSize: 24, @@ -801,7 +832,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { onPressed: () => _showLyricsRefreshDialog( context, ref, - trackAsync, + currentTrack, metadataAsync, musixmatchProviderInstance, neteaseProviderInstance, @@ -835,11 +866,25 @@ class _LyricsAdjustButton extends HookConsumerWidget { void _showLyricsRefreshDialog( BuildContext context, WidgetRef ref, - AsyncValue trackAsync, + CurrentTrackData currentTrack, AsyncValue metadataAsync, musixmatchProvider, 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( context: context, builder: (context) => AlertDialog( @@ -858,24 +903,19 @@ class _LyricsAdjustButton extends HookConsumerWidget { child: ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Re-fetch'), - onPressed: trackAsync.maybeWhen( - data: (track) => track != null - ? () { - Navigator.of(context).pop(); - final metadata = metadataAsync.value; - _showFetchLyricsDialog( - context, - ref, - track, - trackPath, - metadata, - musixmatchProvider, - neteaseProvider, - ); - } - : null, - orElse: () => null, - ), + onPressed: () { + Navigator.of(context).pop(); + final metadata = metadataAsync.value; + _showFetchLyricsDialog( + context, + ref, + track, + currentTrack.path, + metadata, + musixmatchProvider, + neteaseProvider, + ); + }, ), ), Expanded( @@ -886,33 +926,45 @@ class _LyricsAdjustButton extends HookConsumerWidget { backgroundColor: Colors.red, foregroundColor: Colors.white, ), - onPressed: trackAsync.maybeWhen( - data: (track) => track != null - ? () async { - Navigator.of(context).pop(); - debugPrint( - 'Clearing lyrics for track ${track.id}', - ); - final database = ref.read(databaseProvider); - await (database.update( - database.tracks, - )..where((t) => t.id.equals(track.id))).write( - db.TracksCompanion( - lyrics: const drift.Value.absent(), - ), - ); - debugPrint('Cleared lyrics from database'); - // Invalidate the track provider to refresh the UI - ref.invalidate( - trackByPathProvider(trackPath), - ); - debugPrint( - 'Invalidated track provider for $trackPath', - ); - } - : null, - orElse: () => null, - ), + onPressed: () async { + Navigator.of(context).pop(); + debugPrint('Clearing lyrics for track ${track.id}'); + final database = ref.read(databaseProvider); + await (database.update( + database.tracks, + )..where((t) => t.id.equals(track.id))).write( + db.TracksCompanion( + lyrics: const drift.Value.absent(), + ), + ); + debugPrint('Cleared lyrics from database'); + + // Update current track provider if this is the current track + 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( + 'Updated current track provider - cleared lyrics', + ); + } + + // 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( icon: const Icon(Icons.sync), label: const Text('Live Sync Lyrics'), - onPressed: trackAsync.maybeWhen( - data: (track) => track != null - ? () { - Navigator.of(context).pop(); - _showLiveLyricsSyncDialog( - context, - ref, - track, - trackPath, - player, - ); - } - : null, - orElse: () => null, - ), + onPressed: () { + Navigator.of(context).pop(); + _showLiveLyricsSyncDialog( + context, + ref, + track, + currentTrack.path, + player, + ); + }, ), ), SizedBox( @@ -944,20 +991,15 @@ class _LyricsAdjustButton extends HookConsumerWidget { child: ElevatedButton.icon( icon: const Icon(Icons.tune), label: const Text('Manual Offset'), - onPressed: trackAsync.maybeWhen( - data: (track) => track != null - ? () { - Navigator.of(context).pop(); - _showLyricsOffsetDialog( - context, - ref, - track, - trackPath, - ); - } - : null, - orElse: () => null, - ), + onPressed: () { + Navigator.of(context).pop(); + _showLyricsOffsetDialog( + context, + ref, + track, + currentTrack.path, + ); + }, ), ), ], @@ -1243,12 +1285,12 @@ class _QueueView extends HookConsumerWidget { class _TimedLyricsView extends HookConsumerWidget { final LyricsData lyrics; final Player player; - final String trackPath; + final db.Track track; const _TimedLyricsView({ required this.lyrics, required this.player, - required this.trackPath, + required this.track, }); @override @@ -1263,169 +1305,64 @@ class _TimedLyricsView extends HookConsumerWidget { ); final previousIndex = useState(-1); - // Get track data to access lyrics offset - final trackAsync = ref.watch(trackByPathProvider(trackPath)); + // Use track directly to access lyrics offset + 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( + stream: player.stream.position, + initialData: player.state.position, + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + final positionMs = position.inMilliseconds + lyricsOffset; - return StreamBuilder( - stream: player.stream.position, - initialData: player.state.position, - builder: (context, snapshot) { - final position = snapshot.data ?? Duration.zero; - final positionMs = position.inMilliseconds + lyricsOffset; - - // Find current line index - int currentIndex = 0; - for (int i = 0; i < lyrics.lines.length; i++) { - if ((lyrics.lines[i].timeMs ?? 0) <= positionMs) { - currentIndex = i; - } else { - break; - } - } - - // Auto-scroll when current line changes - if (currentIndex != previousIndex.value) { - WidgetsBinding.instance.addPostFrameCallback((_) { - previousIndex.value = currentIndex; - if (isDesktop) { - if (wheelScrollController.hasClients) { - wheelScrollController.animateToItem( - currentIndex, - duration: const Duration(milliseconds: 400), - curve: Curves.easeOutCubic, - ); - } - } else { - listController.animateToItem( - index: currentIndex, - scrollController: scrollController, - alignment: 0.5, - duration: (_) => const Duration(milliseconds: 300), - curve: (_) => Curves.easeOutCubic, - ); - } - }); - } - - final totalDurationMs = player.state.duration.inMilliseconds; + // Find current line index + int currentIndex = 0; + for (int i = 0; i < lyrics.lines.length; i++) { + if ((lyrics.lines[i].timeMs ?? 0) <= positionMs) { + currentIndex = i; + } else { + break; + } + } + // Auto-scroll when current line changes + if (currentIndex != previousIndex.value) { + WidgetsBinding.instance.addPostFrameCallback((_) { + previousIndex.value = currentIndex; if (isDesktop) { - return ListWheelScrollView.useDelegate( - controller: wheelScrollController, - itemExtent: 50, - perspective: 0.002, - offAxisFraction: 1.5, - squeeze: 1.0, - diameterRatio: 2, - physics: const FixedExtentScrollPhysics(), - childDelegate: ListWheelChildBuilderDelegate( - childCount: lyrics.lines.length, - builder: (context, index) { - final line = lyrics.lines[index]; - final isActive = index == currentIndex; - - // Calculate progress within the current line for fill effect - double progress = 0.0; - if (isActive) { - final startTime = line.timeMs ?? 0; - final endTime = index < lyrics.lines.length - 1 - ? (lyrics.lines[index + 1].timeMs ?? startTime) - : totalDurationMs; - if (endTime > startTime) { - progress = - ((positionMs - startTime) / (endTime - startTime)) - .clamp(0.0, 1.0); - } - } - - return Align( - alignment: Alignment.centerRight, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: MediaQuery.sizeOf(context).width * 0.4, - ), - child: InkWell( - onTap: () { - if (line.timeMs != null) { - player.seek(Duration(milliseconds: line.timeMs!)); - } - }, - child: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 32), - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 200), - style: Theme.of(context).textTheme.bodyLarge! - .copyWith( - fontSize: isActive ? 18 : 16, - fontWeight: isActive - ? FontWeight.bold - : FontWeight.normal, - color: isActive - ? Theme.of(context).colorScheme.primary - : Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.7), - ), - textAlign: TextAlign.left, - child: () { - final displayText = line.text; - - return isActive && - progress > 0.0 && - progress < 1.0 - ? ShaderMask( - shaderCallback: (bounds) => - LinearGradient( - colors: [ - Theme.of( - context, - ).colorScheme.primary, - Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.7), - ], - stops: [progress, progress], - ).createShader(bounds), - child: Text( - displayText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ) - : Text( - displayText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - }(), - ), - ), - ), - ), - ); - }, - ), + if (wheelScrollController.hasClients) { + wheelScrollController.animateToItem( + currentIndex, + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutCubic, + ); + } + } else { + listController.animateToItem( + index: currentIndex, + scrollController: scrollController, + alignment: 0.5, + duration: (_) => const Duration(milliseconds: 300), + curve: (_) => Curves.easeOutCubic, ); } + }); + } - return SuperListView.builder( - padding: EdgeInsets.only( - top: 0.25 * MediaQuery.sizeOf(context).height, - bottom: 0.25 * MediaQuery.sizeOf(context).height, - ), - listController: listController, - controller: scrollController, - itemCount: lyrics.lines.length, - itemBuilder: (context, index) { + final totalDurationMs = player.state.duration.inMilliseconds; + + if (isDesktop) { + return ListWheelScrollView.useDelegate( + controller: wheelScrollController, + itemExtent: 50, + perspective: 0.002, + offAxisFraction: 1.5, + squeeze: 1.0, + diameterRatio: 2, + physics: const FixedExtentScrollPhysics(), + childDelegate: ListWheelChildBuilderDelegate( + childCount: lyrics.lines.length, + builder: (context, index) { final line = lyrics.lines[index]; final isActive = index == currentIndex; @@ -1443,52 +1380,138 @@ class _TimedLyricsView extends HookConsumerWidget { } } - return InkWell( - onTap: () { - if (line.timeMs != null) { - player.seek(Duration(milliseconds: line.timeMs!)); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + return Align( + alignment: Alignment.centerRight, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.4, ), - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 200), - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontSize: isActive ? 20 : 16, - fontWeight: isActive - ? FontWeight.bold - : FontWeight.normal, - color: isActive - ? Theme.of(context).colorScheme.primary - : Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.7), - ), - textAlign: TextAlign.center, - child: () { - final displayText = line.text; + child: InkWell( + onTap: () { + if (line.timeMs != null) { + player.seek(Duration(milliseconds: line.timeMs!)); + } + }, + child: Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 32), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: Theme.of(context).textTheme.bodyLarge! + .copyWith( + fontSize: isActive ? 18 : 16, + fontWeight: isActive + ? FontWeight.bold + : FontWeight.normal, + color: isActive + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.7), + ), + textAlign: TextAlign.left, + child: () { + final displayText = line.text; - return isActive && progress > 0.0 && progress < 1.0 - ? ShaderMask( - shaderCallback: (bounds) => LinearGradient( - colors: [ - Theme.of(context).colorScheme.primary, - Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.7), - ], - stops: [progress, progress], - ).createShader(bounds), - child: Text(displayText), - ) - : Text(displayText); - }(), + return isActive && progress > 0.0 && progress < 1.0 + ? ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.7), + ], + stops: [progress, progress], + ).createShader(bounds), + child: Text( + displayText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + : Text( + displayText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }(), + ), + ), ), ), ); }, + ), + ); + } + + return SuperListView.builder( + padding: EdgeInsets.only( + top: 0.25 * MediaQuery.sizeOf(context).height, + bottom: 0.25 * MediaQuery.sizeOf(context).height, + ), + listController: listController, + controller: scrollController, + itemCount: lyrics.lines.length, + itemBuilder: (context, index) { + final line = lyrics.lines[index]; + final isActive = index == currentIndex; + + // Calculate progress within the current line for fill effect + double progress = 0.0; + if (isActive) { + final startTime = line.timeMs ?? 0; + final endTime = index < lyrics.lines.length - 1 + ? (lyrics.lines[index + 1].timeMs ?? startTime) + : totalDurationMs; + if (endTime > startTime) { + progress = ((positionMs - startTime) / (endTime - startTime)) + .clamp(0.0, 1.0); + } + } + + return InkWell( + onTap: () { + if (line.timeMs != null) { + player.seek(Duration(milliseconds: line.timeMs!)); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: isActive ? 20 : 16, + fontWeight: isActive ? FontWeight.bold : FontWeight.normal, + color: isActive + ? Theme.of(context).colorScheme.primary + : Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), + textAlign: TextAlign.center, + child: () { + final displayText = line.text; + + return isActive && progress > 0.0 && progress < 1.0 + ? ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ], + stops: [progress, progress], + ).createShader(bounds), + child: Text(displayText), + ) + : Text(displayText); + }(), + ), + ), ); }, ); @@ -1711,7 +1734,7 @@ class _PlayerControls extends HookWidget { child: Row( spacing: 16, children: [ - _LyricsAdjustButton(trackPath: trackPath, player: player), + _LyricsAdjustButton(player: player), Expanded( child: StreamBuilder( stream: player.stream.volume, @@ -1807,6 +1830,21 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { 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 ref.invalidate(trackByPathProvider(trackPath));