11import 'dart:async' ;
2- import 'dart:typed_data' ;
32
43import 'package:finamp/components/PlayerScreen/queue_source_helper.dart' ;
54import 'package:finamp/components/animated_album_cover.dart' ;
65import 'package:finamp/l10n/app_localizations.dart' ;
76import 'package:finamp/models/finamp_models.dart' ;
87import 'package:finamp/models/jellyfin_models.dart' ;
98import 'package:finamp/services/animated_music_service.dart' ;
9+ import 'package:finamp/services/current_track_metadata_provider.dart' ;
1010import 'package:finamp/services/feedback_helper.dart' ;
1111import 'package:finamp/services/finamp_settings_helper.dart' ;
1212import 'package:finamp/services/music_player_background_task.dart' ;
@@ -29,50 +29,15 @@ class PlayerScreenAlbumImage extends ConsumerStatefulWidget {
2929}
3030
3131class _PlayerScreenAlbumImageState extends ConsumerState <PlayerScreenAlbumImage > {
32- late final AnimatedMusicService _animatedMusicService;
33- String ? _animatedCoverUri;
32+ String ? _currentAnimatedCoverSource;
3433 String ? _lastTrackId;
35- bool _isLoadingAnimatedCover = false ;
36-
37- @override
38- void initState () {
39- super .initState ();
40- // Service is already registered in main.dart - just get the instance
41- _animatedMusicService = GetIt .instance <AnimatedMusicService >();
42- }
43-
44- Future <void > _loadAnimatedCover (String trackId) async {
45- if (_isLoadingAnimatedCover || _lastTrackId == trackId) {
46- return ;
47- }
48-
49- _isLoadingAnimatedCover = true ;
50- _lastTrackId = trackId;
51-
52- try {
53- final animatedUri = await _animatedMusicService.getAnimatedCoverForTrack (BaseItemId (trackId));
54-
55- if (mounted && _lastTrackId == trackId) {
56- setState (() {
57- _animatedCoverUri = animatedUri;
58- });
59- }
60- } catch (e) {
61- // Silently fail and fallback to static image
62- if (mounted) {
63- setState (() {
64- _animatedCoverUri = null ;
65- });
66- }
67- } finally {
68- _isLoadingAnimatedCover = false ;
69- }
70- }
7134
7235 @override
7336 Widget build (BuildContext context) {
7437 final queueService = GetIt .instance <QueueService >();
7538 final audioService = GetIt .instance <MusicPlayerBackgroundTask >();
39+ final animatedMusicService = GetIt .instance <AnimatedMusicService >();
40+
7641 return StreamBuilder <FinampQueueInfo ?>(
7742 stream: queueService.getQueueStream (),
7843 builder: (context, snapshot) {
@@ -85,86 +50,157 @@ class _PlayerScreenAlbumImageState extends ConsumerState<PlayerScreenAlbumImage>
8550 final currentTrack = queueInfo.currentTrack;
8651 final currentTrackId = currentTrack? .baseItemId.raw;
8752
88- // Load animated cover if we have an track ID
89- if (currentTrackId != null && currentTrackId != _lastTrackId) {
90- WidgetsBinding .instance.addPostFrameCallback ((_) {
91- _loadAnimatedCover (currentTrackId);
92- });
53+ // Only proceed if we have a current track
54+ if (currentTrackId == null ) {
55+ if (_currentAnimatedCoverSource != null ) {
56+ _currentAnimatedCoverSource = null ;
57+ if (mounted) setState (() {});
58+ }
59+ return _buildStaticCover (snapshot, audioService);
9360 }
9461
95- return Semantics (
96- label: AppLocalizations .of (
97- context,
98- )! .playerAlbumArtworkTooltip (currentTrack? .item.title ?? AppLocalizations .of (context)! .unknownName),
99- excludeSemantics: true , // replace child semantics with custom semantics
100- container: true ,
101- child: GestureDetector (
102- onSecondaryTapDown: (_) async {
103- var queueItem = snapshot.data! .currentTrack;
104- if (queueItem? .baseItem != null ) {
105- var inPlaylist = queueItemInPlaylist (queueItem);
106- await showModalTrackMenu (
107- context: context,
108- item: queueItem! .baseItem! ,
109- showPlaybackControls: true ,
110- // show controls on player screen
111- parentItem: inPlaylist ? queueItem.source.item : null ,
112- isInPlaylist: inPlaylist,
113- );
114- }
115- },
116- child: SimpleGestureDetector (
117- //TODO replace with PageView, this is just a placeholder
118- onTap: () {
119- unawaited (audioService.togglePlayback ());
120- FeedbackHelper .feedback (FeedbackType .selection);
121- },
122- onDoubleTap: () {
123- final currentTrack = queueService.getCurrentTrack ();
124- if (currentTrack? .baseItem != null && ! FinampSettingsHelper .finampSettings.isOffline) {
125- ref.read (isFavoriteProvider (currentTrack! .baseItem).notifier).toggleFavorite ();
126- }
127- },
128- onHorizontalSwipe: (direction) {
129- if (direction == SwipeDirection .left) {
130- if (! FinampSettingsHelper .finampSettings.disableGesture) {
131- queueService.skipByOffset (1 );
132- FeedbackHelper .feedback (FeedbackType .selection);
133- }
134- } else if (direction == SwipeDirection .right) {
135- if (! FinampSettingsHelper .finampSettings.disableGesture) {
136- queueService.skipByOffset (- 1 );
137- FeedbackHelper .feedback (FeedbackType .selection);
62+ // Watch the metadata provider for the current track
63+ final metadataAsyncValue = ref.watch (currentTrackMetadataProvider);
64+
65+ // Handle metadata loading/error states
66+ final metadata = metadataAsyncValue.unwrapPrevious ();
67+ final metadataValue = metadata.valueOrNull;
68+
69+ // Check if track changed
70+ if (currentTrackId != _lastTrackId) {
71+ _lastTrackId = currentTrackId;
72+
73+ String ? animatedCoverSource;
74+
75+ // First, try to use locally cached file
76+ if (metadataValue? .animatedCoverFile != null && metadataValue! .animatedCoverFile! .existsSync ()) {
77+ animatedCoverSource = metadataValue.animatedCoverFile! .path;
78+ }
79+ // Fallback to online streaming if not in offline mode and no local file
80+ else if (! FinampSettingsHelper .finampSettings.isOffline) {
81+ // Use the animated music service to check if animated cover exists
82+ WidgetsBinding .instance.addPostFrameCallback ((_) async {
83+ try {
84+ final hasAnimatedCover = await animatedMusicService.hasAnimatedCover (BaseItemId (currentTrackId));
85+ if (mounted && hasAnimatedCover && _lastTrackId == currentTrackId) {
86+ final onlineUrl = await animatedMusicService.getAnimatedCoverForTrack (BaseItemId (currentTrackId));
87+ if (mounted && onlineUrl != null && _lastTrackId == currentTrackId) {
88+ setState (() {
89+ _currentAnimatedCoverSource = onlineUrl;
90+ });
13891 }
92+ } else if (mounted && ! hasAnimatedCover) {
93+ // No animated cover available
94+ setState (() {
95+ _currentAnimatedCoverSource = null ;
96+ });
13997 }
140- },
141- child: LayoutBuilder (
142- builder: (context, constraints) {
143- final minPadding = ref.watch (finampSettingsProvider.playerScreenCoverMinimumPadding);
144- final horizontalPadding = constraints.maxWidth * (minPadding / 100.0 );
145- final verticalPadding = constraints.maxHeight * (minPadding / 100.0 );
146-
147- return Padding (
148- padding: EdgeInsets .symmetric (horizontal: horizontalPadding, vertical: verticalPadding),
149- child: _buildCoverWidget (),
150- );
151- },
152- ),
153- ),
154- ),
155- );
98+ } catch (e) {
99+ // Silently fail - no animated cover available for streaming
100+ if (mounted) {
101+ setState (() {
102+ _currentAnimatedCoverSource = null ;
103+ });
104+ }
105+ }
106+ });
107+ } else {
108+ // No animated cover available
109+ animatedCoverSource = null ;
110+ }
111+
112+ // Update animated cover source if we have a local file
113+ if (animatedCoverSource != _currentAnimatedCoverSource) {
114+ _currentAnimatedCoverSource = animatedCoverSource;
115+ if (mounted) setState (() {});
116+ }
117+ }
118+
119+ return _buildCoverWithGestures (snapshot, audioService);
156120 },
157121 );
158122 }
159123
124+ Widget _buildStaticCover (AsyncSnapshot <FinampQueueInfo ?> snapshot, MusicPlayerBackgroundTask audioService) {
125+ return _buildCoverWithGestures (snapshot, audioService);
126+ }
127+
128+ Widget _buildCoverWithGestures (AsyncSnapshot <FinampQueueInfo ?> snapshot, MusicPlayerBackgroundTask audioService) {
129+ final currentTrack = snapshot.data? .currentTrack;
130+
131+ return Semantics (
132+ label: AppLocalizations .of (
133+ context,
134+ )! .playerAlbumArtworkTooltip (currentTrack? .item.title ?? AppLocalizations .of (context)! .unknownName),
135+ excludeSemantics: true , // replace child semantics with custom semantics
136+ container: true ,
137+ child: GestureDetector (
138+ onSecondaryTapDown: (_) async {
139+ var queueItem = snapshot.data! .currentTrack;
140+ if (queueItem? .baseItem != null ) {
141+ var inPlaylist = queueItemInPlaylist (queueItem);
142+ await showModalTrackMenu (
143+ context: context,
144+ item: queueItem! .baseItem! ,
145+ showPlaybackControls: true ,
146+ // show controls on player screen
147+ parentItem: inPlaylist ? queueItem.source.item : null ,
148+ isInPlaylist: inPlaylist,
149+ );
150+ }
151+ },
152+ child: SimpleGestureDetector (
153+ //TODO replace with PageView, this is just a placeholder
154+ onTap: () {
155+ unawaited (audioService.togglePlayback ());
156+ FeedbackHelper .feedback (FeedbackType .selection);
157+ },
158+ onDoubleTap: () {
159+ final queueService = GetIt .instance <QueueService >();
160+ final currentTrack = queueService.getCurrentTrack ();
161+ if (currentTrack? .baseItem != null && ! FinampSettingsHelper .finampSettings.isOffline) {
162+ ref.read (isFavoriteProvider (currentTrack! .baseItem).notifier).toggleFavorite ();
163+ }
164+ },
165+ onHorizontalSwipe: (direction) {
166+ final queueService = GetIt .instance <QueueService >();
167+ if (direction == SwipeDirection .left) {
168+ if (! FinampSettingsHelper .finampSettings.disableGesture) {
169+ queueService.skipByOffset (1 );
170+ FeedbackHelper .feedback (FeedbackType .selection);
171+ }
172+ } else if (direction == SwipeDirection .right) {
173+ if (! FinampSettingsHelper .finampSettings.disableGesture) {
174+ queueService.skipByOffset (- 1 );
175+ FeedbackHelper .feedback (FeedbackType .selection);
176+ }
177+ }
178+ },
179+ child: LayoutBuilder (
180+ builder: (context, constraints) {
181+ final minPadding = ref.watch (finampSettingsProvider.playerScreenCoverMinimumPadding);
182+ final horizontalPadding = constraints.maxWidth * (minPadding / 100.0 );
183+ final verticalPadding = constraints.maxHeight * (minPadding / 100.0 );
184+
185+ return Padding (
186+ padding: EdgeInsets .symmetric (horizontal: horizontalPadding, vertical: verticalPadding),
187+ child: _buildCoverWidget (),
188+ );
189+ },
190+ ),
191+ ),
192+ ),
193+ );
194+ }
195+
160196 Widget _buildCoverWidget () {
161197 final borderRadius = BorderRadius .circular (8.0 );
162198 final decoration = BoxDecoration (
163199 boxShadow: [BoxShadow (blurRadius: 24 , offset: const Offset (0 , 4 ), color: Colors .black.withOpacity (0.3 ))],
164200 );
165201
166202 // Show animated cover if available
167- if (_animatedCoverUri != null ) {
203+ if (_currentAnimatedCoverSource != null ) {
168204 return Stack (
169205 children: [
170206 // Fallback static image
@@ -175,7 +211,7 @@ class _PlayerScreenAlbumImageState extends ConsumerState<PlayerScreenAlbumImage>
175211 decoration: decoration,
176212 ),
177213 // Animated cover overlay
178- AnimatedAlbumCover (animatedCoverUri: _animatedCoverUri ! , borderRadius: borderRadius),
214+ AnimatedAlbumCover (animatedCoverUri: _currentAnimatedCoverSource ! , borderRadius: borderRadius),
179215 ],
180216 );
181217 }
0 commit comments