✨ Lyrics for remote tracks
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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._();
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user