From f3af2cf241264d9517e3f94b559fc1147739be27 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 20 Dec 2025 01:37:00 +0800 Subject: [PATCH] :lipstick: Optimize on tracks --- lib/data/track_repository.g.dart | 2 +- lib/logic/audio_handler.dart | 87 ++++++--- lib/providers/audio_provider.dart | 12 ++ lib/providers/audio_provider.g.dart | 54 ++++++ lib/providers/lrc_fetcher_provider.g.dart | 2 +- lib/ui/widgets/mini_player.dart | 204 ++++++++++++---------- pubspec.lock | 14 +- pubspec.yaml | 1 + 8 files changed, 253 insertions(+), 123 deletions(-) diff --git a/lib/data/track_repository.g.dart b/lib/data/track_repository.g.dart index 7c0c6b6..130c1ae 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'606c68068cb2811a0982c950ba0f12d77cdf9d44'; +String _$trackRepositoryHash() => r'6a8bb9f1b4f29de32d6ad75c311353c4007e139f'; abstract class _$TrackRepository extends $AsyncNotifier { FutureOr build(); diff --git a/lib/logic/audio_handler.dart b/lib/logic/audio_handler.dart index 06ec59b..e18a7d1 100644 --- a/lib/logic/audio_handler.dart +++ b/lib/logic/audio_handler.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; import 'package:audio_service/audio_service.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.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' as db; import 'package:groovybox/logic/metadata_service.dart'; @@ -59,9 +59,17 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { TrackMetadata? metadata; db.Track? track; - // For remote tracks, get metadata from database + // Set loading state for remote tracks final urlResolver = _container!.read(remoteUrlResolverProvider); - if (urlResolver.isProtocolUrl(mediaItem.id)) { + final isRemoteTrack = urlResolver.isProtocolUrl(mediaItem.id); + + final loadingNotifier = _container!.read( + remoteTrackLoadingProvider.notifier, + ); + loadingNotifier.setLoading(true); + + // For remote tracks, get metadata from database + if (isRemoteTrack) { final database = _container!.read(databaseProvider); track = await (database.select( database.tracks, @@ -71,14 +79,10 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { // Fetch album art bytes for remote tracks Uint8List? artBytes; if (track.artUri != null) { - try { - final response = await http.get(Uri.parse(track.artUri!)); - if (response.statusCode == 200) { - artBytes = response.bodyBytes; - } - } catch (e) { - // Ignore art fetching errors - } + final imageFile = await DefaultCacheManager().getSingleFile( + track.artUri!, + ); + artBytes = await imageFile.readAsBytes(); } metadata = TrackMetadata( @@ -110,6 +114,9 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { seedColorNotifier.updateFromAlbumArtBytes(metadata.artBytes); } + // Clear loading state + loadingNotifier.setLoading(false); + // Set current track final trackNotifier = _container!.read(currentTrackProvider.notifier); if (track != null) { @@ -128,6 +135,12 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { metadataNotifier.clear(); } } catch (e) { + // Clear loading state on error + final loadingNotifier = _container!.read( + remoteTrackLoadingProvider.notifier, + ); + loadingNotifier.setLoading(false); + // If metadata retrieval fails, reset to default color and clear metadata/track final seedColorNotifier = _container!.read(seedColorProvider.notifier); seedColorNotifier.resetToDefault(); @@ -252,6 +265,12 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final position = _player.state.position; final duration = _player.state.duration; + // Get current media item metadata if available + MediaItem? currentMediaItem; + if (_queueIndex >= 0 && _queueIndex < _queue.length) { + currentMediaItem = _queue[_queueIndex]; + } + playbackState.add( PlaybackState( controls: [ @@ -274,21 +293,48 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { queueIndex: _queueIndex, ), ); + + // Update media item separately if we have current track info + if (currentMediaItem != null) { + mediaItem.add(currentMediaItem); + } } // New methods that accept Track objects with proper metadata Future playTrack(db.Track track) async { - final mediaItem = _trackToMediaItem(track); + final mediaItem = await _trackToMediaItem(track); await updateQueue([mediaItem]); } Future playTracks(List tracks, {int initialIndex = 0}) async { - final mediaItems = tracks.map(_trackToMediaItem).toList(); + final mediaItems = await Future.wait(tracks.map(_trackToMediaItem)); _queueIndex = initialIndex; await updateQueue(mediaItems); } - MediaItem _trackToMediaItem(db.Track track) { + Future _trackToMediaItem(db.Track track) async { + Uri? artUri; + + if (track.artUri != null) { + // Check if it's a network URL or local file path + if (track.artUri!.startsWith('http://') || + track.artUri!.startsWith('https://')) { + // It's a network URL, cache it and get local file path + try { + final cachedFile = await DefaultCacheManager().getSingleFile( + track.artUri!, + ); + artUri = Uri.file(cachedFile.path); + } catch (e) { + // If caching fails, try to use the network URL directly + artUri = Uri.parse(track.artUri!); + } + } else { + // It's a local file path + artUri = Uri.file(track.artUri!); + } + } + return MediaItem( id: track.path, album: track.album, @@ -297,21 +343,10 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { duration: track.duration != null ? Duration(milliseconds: track.duration!) : null, - artUri: track.artUri != null ? Uri.file(track.artUri!) : null, + artUri: artUri, ); } - // Legacy methods for backward compatibility - Future setSource(String path) async { - final mediaItem = MediaItem( - id: path, - album: 'Unknown Album', - title: _extractTitleFromPath(path), - artist: 'Unknown Artist', - ); - await updateQueue([mediaItem]); - } - Future openPlaylist( List medias, { int initialIndex = 0, diff --git a/lib/providers/audio_provider.dart b/lib/providers/audio_provider.dart index 26cc5db..cbf654b 100644 --- a/lib/providers/audio_provider.dart +++ b/lib/providers/audio_provider.dart @@ -102,3 +102,15 @@ class CurrentTrackMetadataNotifier extends _$CurrentTrackMetadataNotifier { state = null; } } + +@Riverpod(keepAlive: true) +class RemoteTrackLoadingNotifier extends _$RemoteTrackLoadingNotifier { + @override + bool build() { + return false; + } + + void setLoading(bool loading) { + state = loading; + } +} diff --git a/lib/providers/audio_provider.g.dart b/lib/providers/audio_provider.g.dart index b6f2a22..a24ff18 100644 --- a/lib/providers/audio_provider.g.dart +++ b/lib/providers/audio_provider.g.dart @@ -158,3 +158,57 @@ abstract class _$CurrentTrackMetadataNotifier element.handleValue(ref, created); } } + +@ProviderFor(RemoteTrackLoadingNotifier) +const remoteTrackLoadingProvider = RemoteTrackLoadingNotifierProvider._(); + +final class RemoteTrackLoadingNotifierProvider + extends $NotifierProvider { + const RemoteTrackLoadingNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'remoteTrackLoadingProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$remoteTrackLoadingNotifierHash(); + + @$internal + @override + RemoteTrackLoadingNotifier create() => RemoteTrackLoadingNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$remoteTrackLoadingNotifierHash() => + r'e7eda5cbbf3c37e0127960bbc09b121ab3b02afa'; + +abstract class _$RemoteTrackLoadingNotifier extends $Notifier { + bool build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/providers/lrc_fetcher_provider.g.dart b/lib/providers/lrc_fetcher_provider.g.dart index a37f203..4e734e4 100644 --- a/lib/providers/lrc_fetcher_provider.g.dart +++ b/lib/providers/lrc_fetcher_provider.g.dart @@ -41,7 +41,7 @@ final class LyricsFetcherProvider } } -String _$lyricsFetcherHash() => r'071b83cb569812a6f90d42d7b7cf6954ac9631d7'; +String _$lyricsFetcherHash() => r'0b279b6294947cc0460c9fa8a5ef3863e24677f9'; abstract class _$LyricsFetcher extends $Notifier { LyricsFetcherState build(); diff --git a/lib/ui/widgets/mini_player.dart b/lib/ui/widgets/mini_player.dart index aea0e41..b53e37e 100644 --- a/lib/ui/widgets/mini_player.dart +++ b/lib/ui/widgets/mini_player.dart @@ -54,6 +54,7 @@ class _MobileMiniPlayer extends HookConsumerWidget { final devicePadding = MediaQuery.paddingOf(context); final currentMetadata = ref.watch(currentTrackMetadataProvider); + final isRemoteTrackLoading = ref.watch(remoteTrackLoadingProvider); Widget content = Container( height: 72 + devicePadding.bottom, @@ -75,53 +76,66 @@ class _MobileMiniPlayer extends HookConsumerWidget { SizedBox( height: 4, width: double.infinity, - child: StreamBuilder( - stream: player.stream.position, - initialData: player.state.position, - builder: (context, snapshot) { - final position = snapshot.data ?? Duration.zero; - return StreamBuilder( - stream: player.stream.duration, - initialData: player.state.duration, - builder: (context, durationSnapshot) { - final total = durationSnapshot.data ?? Duration.zero; - final max = total.inMilliseconds.toDouble(); - final positionValue = position.inMilliseconds - .toDouble() - .clamp(0.0, max > 0 ? max : 0.0); + child: isRemoteTrackLoading + ? LinearProgressIndicator( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ) + : StreamBuilder( + stream: player.stream.position, + initialData: player.state.position, + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + return StreamBuilder( + stream: player.stream.duration, + initialData: player.state.duration, + builder: (context, durationSnapshot) { + final total = + durationSnapshot.data ?? Duration.zero; + final max = total.inMilliseconds.toDouble(); + final positionValue = position.inMilliseconds + .toDouble() + .clamp(0.0, max > 0 ? max : 0.0); - final currentValue = isDragging.value - ? dragValue.value - : positionValue; + final currentValue = isDragging.value + ? dragValue.value + : positionValue; - return SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 2, - overlayShape: SliderComponentShape.noOverlay, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 6, - ), - trackShape: const RectangularSliderTrackShape(), - ), - child: Slider( - padding: EdgeInsets.zero, - value: currentValue, - min: 0, - max: max > 0 ? max : 1.0, - onChanged: (val) { - isDragging.value = true; - dragValue.value = val; + return SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 2, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6, + ), + trackShape: + const RectangularSliderTrackShape(), + ), + child: Slider( + padding: EdgeInsets.zero, + value: currentValue, + min: 0, + max: max > 0 ? max : 1.0, + onChanged: (val) { + isDragging.value = true; + dragValue.value = val; + }, + onChangeEnd: (val) { + isDragging.value = false; + player.seek( + Duration(milliseconds: val.toInt()), + ); + }, + ), + ); }, - onChangeEnd: (val) { - isDragging.value = false; - player.seek(Duration(milliseconds: val.toInt())); - }, - ), - ); - }, - ); - }, - ), + ); + }, + ), ), Expanded( child: Row( @@ -260,6 +274,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { final devicePadding = MediaQuery.paddingOf(context); final currentMetadata = ref.watch(currentTrackMetadataProvider); + final isRemoteTrackLoading = ref.watch(remoteTrackLoadingProvider); Widget content = Container( height: 72 + devicePadding.bottom, @@ -281,53 +296,66 @@ class _DesktopMiniPlayer extends HookConsumerWidget { SizedBox( height: 4, width: double.infinity, - child: StreamBuilder( - stream: player.stream.position, - initialData: player.state.position, - builder: (context, snapshot) { - final position = snapshot.data ?? Duration.zero; - return StreamBuilder( - stream: player.stream.duration, - initialData: player.state.duration, - builder: (context, durationSnapshot) { - final total = durationSnapshot.data ?? Duration.zero; - final max = total.inMilliseconds.toDouble(); - final positionValue = position.inMilliseconds - .toDouble() - .clamp(0.0, max > 0 ? max : 0.0); + child: isRemoteTrackLoading + ? LinearProgressIndicator( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ) + : StreamBuilder( + stream: player.stream.position, + initialData: player.state.position, + builder: (context, snapshot) { + final position = snapshot.data ?? Duration.zero; + return StreamBuilder( + stream: player.stream.duration, + initialData: player.state.duration, + builder: (context, durationSnapshot) { + final total = + durationSnapshot.data ?? Duration.zero; + final max = total.inMilliseconds.toDouble(); + final positionValue = position.inMilliseconds + .toDouble() + .clamp(0.0, max > 0 ? max : 0.0); - final currentValue = isDragging.value - ? dragValue.value - : positionValue; + final currentValue = isDragging.value + ? dragValue.value + : positionValue; - return SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 2, - overlayShape: SliderComponentShape.noOverlay, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 6, - ), - trackShape: const RectangularSliderTrackShape(), - ), - child: Slider( - padding: EdgeInsets.zero, - value: currentValue, - min: 0, - max: max > 0 ? max : 1.0, - onChanged: (val) { - isDragging.value = true; - dragValue.value = val; + return SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 2, + overlayShape: SliderComponentShape.noOverlay, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6, + ), + trackShape: + const RectangularSliderTrackShape(), + ), + child: Slider( + padding: EdgeInsets.zero, + value: currentValue, + min: 0, + max: max > 0 ? max : 1.0, + onChanged: (val) { + isDragging.value = true; + dragValue.value = val; + }, + onChangeEnd: (val) { + isDragging.value = false; + player.seek( + Duration(milliseconds: val.toInt()), + ); + }, + ), + ); }, - onChangeEnd: (val) { - isDragging.value = false; - player.seek(Duration(milliseconds: val.toInt())); - }, - ), - ); - }, - ); - }, - ), + ); + }, + ), ), Expanded( child: Row( diff --git a/pubspec.lock b/pubspec.lock index 89b4e27..b203529 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -413,10 +413,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "7872545770c277236fd32b022767576c562ba28366204ff1a5628853cf8f2200" + sha256: d974b6ba2606371ac71dd94254beefb6fa81185bde0b59bdc1df09885da85fde url: "https://pub.dev" source: hosted - version: "10.3.7" + version: "10.3.8" fixnum: dependency: transitive description: @@ -431,7 +431,7 @@ packages: source: sdk version: "0.0.0" flutter_cache_manager: - dependency: transitive + dependency: "direct main" description: name: flutter_cache_manager sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" @@ -588,10 +588,10 @@ packages: dependency: transitive description: name: image - sha256: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332" + sha256: "48c11d0943b93b6fb29103d956ff89aafeae48f6058a3939649be2093dcff0bf" url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.7.1" io: dependency: transitive description: @@ -1281,10 +1281,10 @@ packages: dependency: transitive description: name: uri_parser - sha256: "380c6c96c52a8de82c84c627d0a79cfb2e1697c3fe3eda78fbb9b87af0dcee08" + sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f1b3302..ff12582 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: shared_preferences: ^2.3.5 jellyfin_dart: ^0.1.2 cached_network_image: ^3.4.1 + flutter_cache_manager: ^3.4.1 dev_dependencies: flutter_test: