@@ -8,6 +8,7 @@ import 'package:classipod/core/widgets/empty_state_widget.dart';
88import 'package:classipod/features/device/models/device_action.dart' ;
99import 'package:classipod/features/device/services/device_buttons_service_provider.dart' ;
1010import 'package:classipod/features/now_playing/provider/now_playing_details_provider.dart' ;
11+ import 'package:classipod/features/now_playing/widgets/lyrics_view.dart' ;
1112import 'package:classipod/features/now_playing/widgets/now_playing_bottom_bar.dart' ;
1213import 'package:classipod/features/now_playing/widgets/now_playing_widget.dart' ;
1314import 'package:classipod/features/now_playing/widgets/rating_bar.dart' ;
@@ -27,6 +28,7 @@ enum _NowPlayingBottomBarPage {
2728 volumeBar,
2829 shuffleBar,
2930 ratingBar,
31+ lyrics,
3032}
3133
3234class NowPlayingScreen extends ConsumerStatefulWidget {
@@ -38,14 +40,22 @@ class NowPlayingScreen extends ConsumerStatefulWidget {
3840
3941class _NowPlayingScreenState extends ConsumerState <NowPlayingScreen > {
4042 final PageController _bottomBarPageController = PageController ();
43+ final ScrollController _lyricsScrollController = ScrollController ();
4144 late Timer _longPressTimer;
4245 Timer _lastVolumeChangeTimer = Timer (Duration .zero, () {});
4346 bool _isShuffleEnabled = false ;
4447 _NowPlayingBottomBarPage _bottomBarPage = _NowPlayingBottomBarPage .seekBar;
48+ int ? _lastLyricsSongIndex;
4549
4650 String get routeName => Routes .nowPlaying.name;
4751
4852 Future <void > onSelectPressed () async {
53+ final String ? lyrics = ref
54+ .read (nowPlayingDetailsProvider)
55+ .currentMetadata
56+ ? .lyrics;
57+ final bool hasLyrics = lyrics != null && lyrics.trim ().isNotEmpty;
58+
4959 if (_bottomBarPage == _NowPlayingBottomBarPage .seekBar) {
5060 await _bottomBarPageController.animateToPage (
5161 1 ,
@@ -77,6 +87,26 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
7787 _bottomBarPage = _NowPlayingBottomBarPage .ratingBar;
7888 });
7989 } else if (_bottomBarPage == _NowPlayingBottomBarPage .ratingBar) {
90+ if (hasLyrics) {
91+ await _bottomBarPageController.animateToPage (
92+ 4 ,
93+ duration: const Duration (milliseconds: 300 ),
94+ curve: Curves .ease,
95+ );
96+ setState (() {
97+ _bottomBarPage = _NowPlayingBottomBarPage .lyrics;
98+ });
99+ } else {
100+ await _bottomBarPageController.animateToPage (
101+ 0 ,
102+ duration: const Duration (milliseconds: 300 ),
103+ curve: Curves .ease,
104+ );
105+ setState (() {
106+ _bottomBarPage = _NowPlayingBottomBarPage .seekBar;
107+ });
108+ }
109+ } else if (_bottomBarPage == _NowPlayingBottomBarPage .lyrics) {
80110 await _bottomBarPageController.animateToPage (
81111 0 ,
82112 duration: const Duration (milliseconds: 300 ),
@@ -92,6 +122,28 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
92122 unawaited (context.pushNamed (Routes .nowPlayingMoreOptions.name));
93123 }
94124
125+ Future <void > scrollLyrics (double offsetChange) async {
126+ if (! _lyricsScrollController.hasClients) {
127+ return ;
128+ }
129+
130+ final ScrollPosition position = _lyricsScrollController.position;
131+ final double targetOffset = (position.pixels + offsetChange).clamp (
132+ position.minScrollExtent,
133+ position.maxScrollExtent,
134+ );
135+
136+ if (targetOffset == position.pixels) {
137+ return ;
138+ }
139+
140+ await _lyricsScrollController.animateTo (
141+ targetOffset,
142+ duration: const Duration (milliseconds: 200 ),
143+ curve: Curves .easeOut,
144+ );
145+ }
146+
95147 void startVolumeTimer () {
96148 if (_lastVolumeChangeTimer.isActive) {
97149 _lastVolumeChangeTimer.cancel ();
@@ -122,6 +174,9 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
122174 .read (nowPlayingDetailsProvider.notifier)
123175 .increaseCurrentMetadataRating ();
124176 return ;
177+ } else if (_bottomBarPage == _NowPlayingBottomBarPage .lyrics) {
178+ await scrollLyrics (60 );
179+ return ;
125180 }
126181 setState (() {
127182 _bottomBarPage = _NowPlayingBottomBarPage .volumeBar;
@@ -146,6 +201,9 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
146201 .read (nowPlayingDetailsProvider.notifier)
147202 .decreaseCurrentMetadataRating ();
148203 return ;
204+ } else if (_bottomBarPage == _NowPlayingBottomBarPage .lyrics) {
205+ await scrollLyrics (- 60 );
206+ return ;
149207 }
150208 setState (() {
151209 _bottomBarPage = _NowPlayingBottomBarPage .volumeBar;
@@ -203,6 +261,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
203261 void dispose () {
204262 _lastVolumeChangeTimer.cancel ();
205263 _bottomBarPageController.dispose ();
264+ _lyricsScrollController.dispose ();
206265 super .dispose ();
207266 }
208267
@@ -249,6 +308,34 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
249308 @override
250309 Widget build (BuildContext context) {
251310 final nowPlayingDetails = ref.watch (nowPlayingDetailsProvider);
311+ final String ? lyrics = nowPlayingDetails.currentMetadata? .lyrics;
312+ final bool hasLyrics = lyrics != null && lyrics.trim ().isNotEmpty;
313+ final int ? currentLyricsSongIndex =
314+ nowPlayingDetails.currentMetadata? .originalSongIndex;
315+
316+ if (_lastLyricsSongIndex != currentLyricsSongIndex) {
317+ _lastLyricsSongIndex = currentLyricsSongIndex;
318+ WidgetsBinding .instance.addPostFrameCallback ((_) {
319+ if (! _lyricsScrollController.hasClients) {
320+ return ;
321+ }
322+ _lyricsScrollController.jumpTo (0 );
323+ });
324+ }
325+
326+ if (! hasLyrics && _bottomBarPage == _NowPlayingBottomBarPage .lyrics) {
327+ WidgetsBinding .instance.addPostFrameCallback ((_) {
328+ if (! mounted) {
329+ return ;
330+ }
331+ if (_bottomBarPageController.hasClients) {
332+ _bottomBarPageController.jumpToPage (0 );
333+ }
334+ setState (() {
335+ _bottomBarPage = _NowPlayingBottomBarPage .seekBar;
336+ });
337+ });
338+ }
252339
253340 ref.listen (deviceButtonsServiceProvider, deviceControlHandler);
254341
@@ -305,7 +392,23 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
305392 onLongPress: onSelectLongPress,
306393 child: Padding (
307394 padding: const EdgeInsets .only (left: 10 ),
308- child: NowPlayingWidget (nowPlayingDetails: nowPlayingDetails),
395+ child: AnimatedSwitcher (
396+ duration: const Duration (milliseconds: 300 ),
397+ child:
398+ (_bottomBarPage == _NowPlayingBottomBarPage .lyrics &&
399+ hasLyrics)
400+ ? LyricsView (
401+ key: ValueKey (
402+ 'lyrics-view-${nowPlayingDetails .currentMetadata ?.originalSongIndex ?? 0 }' ,
403+ ),
404+ lyrics: lyrics,
405+ scrollController: _lyricsScrollController,
406+ )
407+ : NowPlayingWidget (
408+ key: const ValueKey ('now-playing-view' ),
409+ nowPlayingDetails: nowPlayingDetails,
410+ ),
411+ ),
309412 ),
310413 ),
311414 ),
@@ -360,6 +463,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
360463 .setCurrentMetadataRating (val ?? 0 );
361464 },
362465 ),
466+ if (hasLyrics) const SizedBox (height: 10 ),
363467 ],
364468 ),
365469 ),
0 commit comments