✨ User library
This commit is contained in:
		| @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:rhythm_box/screens/auth/mobile_login.dart'; | ||||
| import 'package:rhythm_box/screens/explore.dart'; | ||||
| import 'package:rhythm_box/screens/library/view.dart'; | ||||
| import 'package:rhythm_box/screens/player/lyrics.dart'; | ||||
| import 'package:rhythm_box/screens/player/view.dart'; | ||||
| import 'package:rhythm_box/screens/playlist/view.dart'; | ||||
| @@ -19,6 +20,11 @@ final router = GoRouter(routes: [ | ||||
|         name: 'explore', | ||||
|         builder: (context, state) => const ExploreScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/library', | ||||
|         name: 'library', | ||||
|         builder: (context, state) => const LibraryScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/search', | ||||
|         name: 'search', | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:rhythm_box/providers/spotify.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:rhythm_box/widgets/playlist/playlist_tile.dart'; | ||||
| import 'package:rhythm_box/widgets/sized_container.dart'; | ||||
| import 'package:skeletonizer/skeletonizer.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
| @@ -49,29 +49,8 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|               itemCount: _featuredPlaylist?.length ?? 20, | ||||
|               itemBuilder: (context, idx) { | ||||
|                 final item = _featuredPlaylist?[idx]; | ||||
|                 return ListTile( | ||||
|                   leading: ClipRRect( | ||||
|                     borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                     child: item != null | ||||
|                         ? AutoCacheImage( | ||||
|                             item.images!.first.url!, | ||||
|                             width: 64.0, | ||||
|                             height: 64.0, | ||||
|                           ) | ||||
|                         : const SizedBox( | ||||
|                             width: 64, | ||||
|                             height: 64, | ||||
|                             child: Center( | ||||
|                               child: Icon(Icons.image), | ||||
|                             ), | ||||
|                           ), | ||||
|                   ), | ||||
|                   title: Text(item?.name ?? 'Loading...'), | ||||
|                   subtitle: Text( | ||||
|                     item?.description ?? 'Please stand by...', | ||||
|                     maxLines: 2, | ||||
|                     overflow: TextOverflow.ellipsis, | ||||
|                   ), | ||||
|                 return PlaylistTile( | ||||
|                   item: item, | ||||
|                   onTap: () { | ||||
|                     if (item == null) return; | ||||
|                     GoRouter.of(context).pushNamed( | ||||
|   | ||||
							
								
								
									
										36
									
								
								lib/screens/library/view.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								lib/screens/library/view.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:rhythm_box/providers/auth.dart'; | ||||
| import 'package:rhythm_box/widgets/no_login_fallback.dart'; | ||||
| import 'package:rhythm_box/widgets/playlist/user_playlist_list.dart'; | ||||
|  | ||||
| class LibraryScreen extends StatefulWidget { | ||||
|   const LibraryScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<LibraryScreen> createState() => _LibraryScreenState(); | ||||
| } | ||||
|  | ||||
| class _LibraryScreenState extends State<LibraryScreen> { | ||||
|   late final AuthenticationProvider _authenticate = Get.find(); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Material( | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: SafeArea( | ||||
|         child: Obx(() { | ||||
|           if (_authenticate.auth.value == null) { | ||||
|             return const NoLoginFallback(); | ||||
|           } | ||||
|  | ||||
|           return const Column( | ||||
|             children: [ | ||||
|               Expanded(child: UserPlaylistList()), | ||||
|             ], | ||||
|           ); | ||||
|         }), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:rhythm_box/widgets/lyrics/synced.dart'; | ||||
| import 'package:rhythm_box/widgets/lyrics/synced_lyrics.dart'; | ||||
| import 'package:rhythm_box/widgets/player/bottom_player.dart'; | ||||
|  | ||||
| class LyricsScreen extends StatelessWidget { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import 'package:rhythm_box/services/audio_player/audio_player.dart'; | ||||
| import 'package:rhythm_box/services/duration.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:rhythm_box/services/audio_services/image.dart'; | ||||
| import 'package:rhythm_box/widgets/lyrics/synced.dart'; | ||||
| import 'package:rhythm_box/widgets/lyrics/synced_lyrics.dart'; | ||||
| import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; | ||||
|  | ||||
| class PlayerScreen extends StatefulWidget { | ||||
| @@ -75,7 +75,8 @@ class _PlayerScreenState extends State<PlayerScreen> { | ||||
|                 child: Column( | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     LimitedBox( | ||||
|                     Obx( | ||||
|                       () => LimitedBox( | ||||
|                         maxHeight: maxAlbumSize, | ||||
|                         maxWidth: maxAlbumSize, | ||||
|                         child: Hero( | ||||
| @@ -97,23 +98,32 @@ class _PlayerScreenState extends State<PlayerScreen> { | ||||
|                                           .surfaceContainerHigh, | ||||
|                                       width: 64, | ||||
|                                       height: 64, | ||||
|                                     child: | ||||
|                                         const Center(child: Icon(Icons.image)), | ||||
|                                       child: const Center( | ||||
|                                           child: Icon(Icons.image)), | ||||
|                                     ), | ||||
|                             ), | ||||
|                           ).marginSymmetric(horizontal: 24), | ||||
|                         ), | ||||
|                       ), | ||||
|                     const Gap(24), | ||||
|                     Text( | ||||
|                       _playback.state.value.activeTrack?.name ?? 'Not playing', | ||||
|                       style: Theme.of(context).textTheme.titleLarge, | ||||
|                     ), | ||||
|                     Text( | ||||
|                       _playback.state.value.activeTrack?.artists?.asString() ?? | ||||
|                     const Gap(24), | ||||
|                     Obx( | ||||
|                       () => Text( | ||||
|                         _playback.state.value.activeTrack?.name ?? | ||||
|                             'Not playing', | ||||
|                         style: Theme.of(context).textTheme.titleLarge, | ||||
|                         textAlign: TextAlign.center, | ||||
|                       ), | ||||
|                     ), | ||||
|                     Obx( | ||||
|                       () => Text( | ||||
|                         _playback.state.value.activeTrack?.artists | ||||
|                                 ?.asString() ?? | ||||
|                             'No author', | ||||
|                         style: Theme.of(context).textTheme.bodyMedium, | ||||
|                         overflow: TextOverflow.ellipsis, | ||||
|                         textAlign: TextAlign.center, | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Gap(24), | ||||
|                     Obx( | ||||
| @@ -197,14 +207,17 @@ class _PlayerScreenState extends State<PlayerScreen> { | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                         IconButton( | ||||
|                         Obx( | ||||
|                           () => IconButton( | ||||
|                             icon: const Icon(Icons.skip_previous), | ||||
|                             onPressed: _isFetchingActiveTrack | ||||
|                                 ? null | ||||
|                                 : audioPlayer.skipToPrevious, | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Gap(8), | ||||
|                         SizedBox( | ||||
|                         Obx( | ||||
|                           () => SizedBox( | ||||
|                             width: 56, | ||||
|                             height: 56, | ||||
|                             child: IconButton.filled( | ||||
| @@ -228,13 +241,16 @@ class _PlayerScreenState extends State<PlayerScreen> { | ||||
|                                   : _togglePlayState, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Gap(8), | ||||
|                         IconButton( | ||||
|                         Obx( | ||||
|                           () => IconButton( | ||||
|                             icon: const Icon(Icons.skip_next), | ||||
|                             onPressed: _isFetchingActiveTrack | ||||
|                                 ? null | ||||
|                                 : audioPlayer.skipToNext, | ||||
|                           ), | ||||
|                         ), | ||||
|                         Obx( | ||||
|                           () => IconButton( | ||||
|                             icon: Icon( | ||||
|   | ||||
| @@ -37,19 +37,46 @@ class _PlaylistViewScreenState extends State<PlaylistViewScreen> { | ||||
|       : false; | ||||
|  | ||||
|   bool _isLoading = true; | ||||
|   bool _isLoadingTracks = true; | ||||
|   bool _isUpdating = false; | ||||
|  | ||||
|   Playlist? _playlist; | ||||
|   List<Track>? _tracks; | ||||
|  | ||||
|   Future<void> _pullPlaylist() async { | ||||
|     if (widget.playlistId == 'user-liked-tracks') { | ||||
|       _playlist = Playlist() | ||||
|         ..name = 'Liked Music' | ||||
|         ..description = 'Your favorite music' | ||||
|         ..type = 'playlist' | ||||
|         ..collaborative = false | ||||
|         ..public = false | ||||
|         ..id = 'user-liked-tracks'; | ||||
|     } else { | ||||
|       _playlist = await _spotify.api.playlists.get(widget.playlistId); | ||||
|     } | ||||
|     setState(() => _isLoading = false); | ||||
|   } | ||||
|  | ||||
|   Future<void> _pullTracks() async { | ||||
|     if (widget.playlistId == 'user-liked-tracks') { | ||||
|       _tracks = (await _spotify.api.tracks.me.saved.all()) | ||||
|           .map((x) => x.track!) | ||||
|           .toList(); | ||||
|     } else { | ||||
|       _tracks = (await _spotify.api.playlists | ||||
|               .getTracksByPlaylistId(widget.playlistId) | ||||
|               .all()) | ||||
|           .toList(); | ||||
|     } | ||||
|     setState(() => _isLoadingTracks = false); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _pullPlaylist(); | ||||
|     _pullTracks(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -86,13 +113,16 @@ class _PlaylistViewScreenState extends State<PlaylistViewScreen> { | ||||
|                               elevation: 2, | ||||
|                               child: ClipRRect( | ||||
|                                 borderRadius: radius, | ||||
|                                 child: Hero( | ||||
|                                   tag: Key('playlist-cover-${_playlist!.id}'), | ||||
|                                   child: AutoCacheImage( | ||||
|                                 child: (_playlist?.images?.isNotEmpty ?? false) | ||||
|                                     ? AutoCacheImage( | ||||
|                                         _playlist!.images!.first.url!, | ||||
|                                         width: 160.0, | ||||
|                                         height: 160.0, | ||||
|                                   ), | ||||
|                                       ) | ||||
|                                     : const SizedBox( | ||||
|                                         width: 160, | ||||
|                                         height: 160, | ||||
|                                         child: Icon(Icons.image), | ||||
|                                       ), | ||||
|                               ), | ||||
|                             ), | ||||
| @@ -116,7 +146,7 @@ class _PlaylistViewScreenState extends State<PlaylistViewScreen> { | ||||
|                                   ), | ||||
|                                   const Gap(8), | ||||
|                                   Text( | ||||
|                                     "${NumberFormat.compactCurrency(symbol: '', decimalDigits: 2).format(_playlist!.followers!.total!)} saves", | ||||
|                                     "${NumberFormat.compactCurrency(symbol: '', decimalDigits: 2).format(_playlist!.followers?.total! ?? 0)} saves", | ||||
|                                   ), | ||||
|                                   Text( | ||||
|                                     '#${_playlist!.id}', | ||||
| @@ -153,14 +183,7 @@ class _PlaylistViewScreenState extends State<PlaylistViewScreen> { | ||||
|  | ||||
|                                         setState(() => _isUpdating = true); | ||||
|  | ||||
|                                         final tracks = (await _spotify | ||||
|                                                 .api.playlists | ||||
|                                                 .getTracksByPlaylistId( | ||||
|                                                     widget.playlistId) | ||||
|                                                 .all()) | ||||
|                                             .toList(); | ||||
|  | ||||
|                                         await _playback.load(tracks, | ||||
|                                         await _playback.load(_tracks!, | ||||
|                                             autoPlay: true); | ||||
|                                         _playback.addCollection(_playlist!.id!); | ||||
|                                         Get.find<PlaybackHistoryProvider>() | ||||
| @@ -180,18 +203,11 @@ class _PlaylistViewScreenState extends State<PlaylistViewScreen> { | ||||
|  | ||||
|                                       audioPlayer.setShuffle(true); | ||||
|  | ||||
|                                       final tracks = (await _spotify | ||||
|                                               .api.playlists | ||||
|                                               .getTracksByPlaylistId( | ||||
|                                                   widget.playlistId) | ||||
|                                               .all()) | ||||
|                                           .toList(); | ||||
|  | ||||
|                                       await _playback.load( | ||||
|                                         tracks, | ||||
|                                         _tracks!, | ||||
|                                         autoPlay: true, | ||||
|                                         initialIndex: | ||||
|                                             Random().nextInt(tracks.length), | ||||
|                                             Random().nextInt(_tracks!.length), | ||||
|                                       ); | ||||
|                                       _playback.addCollection(_playlist!.id!); | ||||
|                                       Get.find<PlaybackHistoryProvider>() | ||||
| @@ -208,11 +224,15 @@ class _PlaylistViewScreenState extends State<PlaylistViewScreen> { | ||||
|                   ), | ||||
|                   SliverToBoxAdapter( | ||||
|                     child: Text( | ||||
|                       'Songs (${_playlist!.tracks!.total})', | ||||
|                       'Songs (${_playlist!.tracks?.total ?? (_tracks?.length ?? 0)})', | ||||
|                       style: Theme.of(context).textTheme.titleLarge, | ||||
|                     ).paddingOnly(left: 28, right: 28, bottom: 4), | ||||
|                   ), | ||||
|                   PlaylistTrackList(playlistId: widget.playlistId), | ||||
|                   PlaylistTrackList( | ||||
|                     isLoading: _isLoadingTracks, | ||||
|                     playlistId: widget.playlistId, | ||||
|                     tracks: _tracks, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ); | ||||
|   | ||||
| @@ -45,8 +45,6 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|               return FutureBuilder( | ||||
|                 future: _spotify.api.me.get(), | ||||
|                 builder: (context, snapshot) { | ||||
|                   print(snapshot.data); | ||||
|                   print(snapshot.error); | ||||
|                   if (!snapshot.hasData) { | ||||
|                     return const ListTile( | ||||
|                       contentPadding: | ||||
|   | ||||
| @@ -24,6 +24,7 @@ class _NavShellState extends State<NavShell> { | ||||
|  | ||||
|   final List<Destination> _allDestinations = <Destination>[ | ||||
|     Destination('explore'.tr, 'explore', Icons.explore), | ||||
|     Destination('library'.tr, 'library', Icons.video_library), | ||||
|     Destination('search'.tr, 'search', Icons.search), | ||||
|     Destination('settings'.tr, 'settings', Icons.settings), | ||||
|   ]; | ||||
| @@ -40,6 +41,7 @@ class _NavShellState extends State<NavShell> { | ||||
|             const BottomPlayer(key: Key('app-wide-bottom-player')), | ||||
|             const Divider(height: 0.3, thickness: 0.3), | ||||
|             BottomNavigationBar( | ||||
|               type: BottomNavigationBarType.fixed, | ||||
|               landscapeLayout: BottomNavigationBarLandscapeLayout.centered, | ||||
|               elevation: 0, | ||||
|               showUnselectedLabels: false, | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| const i18nEnglish = { | ||||
|   'appName': 'RhythmBox', | ||||
|   'explore': 'Explore', | ||||
|   'library': 'Library', | ||||
|   'settings': 'Settings', | ||||
|   'search': 'Search', | ||||
| }; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| const i18nSimplifiedChinese = { | ||||
|   'appName': '韵律盒', | ||||
|   'explore': '探索', | ||||
|   'library': '资料库', | ||||
|   'settings': '设置', | ||||
|   'search': '搜索', | ||||
| }; | ||||
|   | ||||
| @@ -28,15 +28,16 @@ class _SyncedLyricsState extends State<SyncedLyrics> { | ||||
|   final AutoScrollController _autoScrollController = AutoScrollController(); | ||||
| 
 | ||||
|   late final int _textZoomLevel = widget.defaultTextZoom; | ||||
|   late Duration _durationCurrent = audioPlayer.position; | ||||
| 
 | ||||
|   SubtitleSimple? _lyric; | ||||
|   String? _activeTrackId; | ||||
| 
 | ||||
|   bool get _isLyricSynced => | ||||
|       _lyric == null ? false : _lyric!.lyrics.any((x) => x.time.inSeconds > 0); | ||||
| 
 | ||||
|   Future<void> _pullLyrics() async { | ||||
|     if (_playback.state.value.activeTrack == null) return; | ||||
|     _activeTrackId = _playback.state.value.activeTrack!.id; | ||||
|     final out = await _syncedLyrics.fetch(_playback.state.value.activeTrack!); | ||||
|     setState(() => _lyric = out); | ||||
|   } | ||||
| @@ -49,11 +50,15 @@ class _SyncedLyricsState extends State<SyncedLyrics> { | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _subscriptions = [ | ||||
|       audioPlayer.positionStream | ||||
|           .listen((dur) => setState(() => _durationCurrent = dur)), | ||||
|     ]; | ||||
|     _pullLyrics(); | ||||
|     _subscriptions = [ | ||||
|       _playback.state.listen((value) { | ||||
|         if (value.activeTrack == null) return; | ||||
|         if (value.activeTrack!.id != _activeTrackId) { | ||||
|           _pullLyrics(); | ||||
|         } | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
| @@ -77,18 +82,19 @@ class _SyncedLyricsState extends State<SyncedLyrics> { | ||||
|         if (_lyric != null && _lyric!.lyrics.isNotEmpty) | ||||
|           SliverList.builder( | ||||
|             itemCount: _lyric!.lyrics.length, | ||||
|             itemBuilder: (context, idx) { | ||||
|             itemBuilder: (context, idx) => Obx(() { | ||||
|               final lyricSlice = _lyric!.lyrics[idx]; | ||||
|               final lyricNextSlice = idx + 1 < _lyric!.lyrics.length | ||||
|                   ? _lyric!.lyrics[idx + 1] | ||||
|                   : null; | ||||
|               final isActive = | ||||
|                   _durationCurrent.inSeconds >= lyricSlice.time.inSeconds && | ||||
|               final isActive = _playback.durationCurrent.value.inSeconds >= | ||||
|                       lyricSlice.time.inSeconds && | ||||
|                   (lyricNextSlice == null || | ||||
|                       lyricNextSlice.time.inSeconds > | ||||
|                               _durationCurrent.inSeconds); | ||||
|                           _playback.durationCurrent.value.inSeconds); | ||||
| 
 | ||||
|               if (_durationCurrent.inSeconds == lyricSlice.time.inSeconds && | ||||
|               if (_playback.durationCurrent.value.inSeconds == | ||||
|                       lyricSlice.time.inSeconds && | ||||
|                   _isLyricSynced) { | ||||
|                 _autoScrollController.scrollToIndex( | ||||
|                   idx, | ||||
| @@ -150,7 +156,7 @@ class _SyncedLyricsState extends State<SyncedLyrics> { | ||||
|                         ), | ||||
|                       ), | ||||
|               ); | ||||
|             }, | ||||
|             }), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
							
								
								
									
										32
									
								
								lib/widgets/no_login_fallback.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/widgets/no_login_fallback.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:rhythm_box/widgets/sized_container.dart'; | ||||
|  | ||||
| class NoLoginFallback extends StatelessWidget { | ||||
|   const NoLoginFallback({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return CenteredContainer( | ||||
|       maxWidth: 280, | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           const Icon( | ||||
|             Icons.login, | ||||
|             size: 48, | ||||
|           ), | ||||
|           const Gap(12), | ||||
|           Text( | ||||
|             'Connect with your Spotify', | ||||
|             style: Theme.of(context).textTheme.titleLarge, | ||||
|           ), | ||||
|           const Text( | ||||
|             'You need to connect RhythmBox with your spotify account in settings page, so that we can access your library.', | ||||
|             textAlign: TextAlign.center, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										43
									
								
								lib/widgets/playlist/playlist_tile.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								lib/widgets/playlist/playlist_tile.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
|  | ||||
| class PlaylistTile extends StatelessWidget { | ||||
|   final PlaylistSimple? item; | ||||
|  | ||||
|   final Function? onTap; | ||||
|  | ||||
|   const PlaylistTile({super.key, required this.item, this.onTap}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListTile( | ||||
|       leading: ClipRRect( | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         child: (item?.images?.isNotEmpty ?? false) | ||||
|             ? AutoCacheImage( | ||||
|                 item!.images!.first.url!, | ||||
|                 width: 64.0, | ||||
|                 height: 64.0, | ||||
|               ) | ||||
|             : const SizedBox( | ||||
|                 width: 64, | ||||
|                 height: 64, | ||||
|                 child: Center( | ||||
|                   child: Icon(Icons.image), | ||||
|                 ), | ||||
|               ), | ||||
|       ), | ||||
|       title: Text(item?.name ?? 'Loading...'), | ||||
|       subtitle: Text( | ||||
|         item?.description ?? 'Please stand by...', | ||||
|         maxLines: 2, | ||||
|         overflow: TextOverflow.ellipsis, | ||||
|       ), | ||||
|       onTap: () { | ||||
|         if (onTap == null) return; | ||||
|         onTap!(); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										64
									
								
								lib/widgets/playlist/user_playlist_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								lib/widgets/playlist/user_playlist_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:rhythm_box/providers/spotify.dart'; | ||||
| import 'package:rhythm_box/widgets/playlist/playlist_tile.dart'; | ||||
| import 'package:skeletonizer/skeletonizer.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
|  | ||||
| class UserPlaylistList extends StatefulWidget { | ||||
|   const UserPlaylistList({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<UserPlaylistList> createState() => _UserPlaylistListState(); | ||||
| } | ||||
|  | ||||
| class _UserPlaylistListState extends State<UserPlaylistList> { | ||||
|   late final SpotifyProvider _spotify = Get.find(); | ||||
|  | ||||
|   PlaylistSimple get _userLikedPlaylist => PlaylistSimple() | ||||
|     ..name = 'Liked Music' | ||||
|     ..description = 'Your favorite music' | ||||
|     ..type = 'playlist' | ||||
|     ..collaborative = false | ||||
|     ..public = false | ||||
|     ..id = 'user-liked-tracks'; | ||||
|  | ||||
|   bool _isLoading = true; | ||||
|  | ||||
|   List<PlaylistSimple>? _playlist; | ||||
|  | ||||
|   Future<void> _pullPlaylist() async { | ||||
|     _playlist = [_userLikedPlaylist, ...await _spotify.api.playlists.me.all()]; | ||||
|     setState(() => _isLoading = false); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _pullPlaylist(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Skeletonizer( | ||||
|       enabled: _isLoading, | ||||
|       child: ListView.builder( | ||||
|         itemCount: _playlist?.length ?? 3, | ||||
|         itemBuilder: (context, idx) { | ||||
|           final item = _playlist?[idx]; | ||||
|           return PlaylistTile( | ||||
|             item: item, | ||||
|             onTap: () { | ||||
|               if (item == null) return; | ||||
|               GoRouter.of(context).pushNamed( | ||||
|                 'playlistView', | ||||
|                 pathParameters: {'id': item.id!}, | ||||
|               ); | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,82 +1,42 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:rhythm_box/providers/audio_player.dart'; | ||||
| import 'package:rhythm_box/providers/spotify.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:rhythm_box/widgets/tracks/track_tile.dart'; | ||||
| import 'package:skeletonizer/skeletonizer.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
| import 'package:rhythm_box/services/artist.dart'; | ||||
|  | ||||
| class PlaylistTrackList extends StatefulWidget { | ||||
| class PlaylistTrackList extends StatelessWidget { | ||||
|   final String playlistId; | ||||
|   final List<Track>? tracks; | ||||
|  | ||||
|   const PlaylistTrackList({super.key, required this.playlistId}); | ||||
|   final bool isLoading; | ||||
|  | ||||
|   @override | ||||
|   State<PlaylistTrackList> createState() => _PlaylistTrackListState(); | ||||
| } | ||||
|  | ||||
| class _PlaylistTrackListState extends State<PlaylistTrackList> { | ||||
|   late final SpotifyProvider _spotify = Get.find(); | ||||
|  | ||||
|   bool _isLoading = true; | ||||
|  | ||||
|   List<Track>? _tracks; | ||||
|  | ||||
|   Future<void> _pullTracks() async { | ||||
|     _tracks = (await _spotify.api.playlists | ||||
|             .getTracksByPlaylistId(widget.playlistId) | ||||
|             .all()) | ||||
|         .toList(); | ||||
|     setState(() => _isLoading = false); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     _pullTracks(); | ||||
|     super.initState(); | ||||
|   } | ||||
|   const PlaylistTrackList({ | ||||
|     super.key, | ||||
|     this.isLoading = false, | ||||
|     required this.playlistId, | ||||
|     required this.tracks, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Skeletonizer.sliver( | ||||
|       enabled: _isLoading, | ||||
|       enabled: isLoading, | ||||
|       child: SliverList.builder( | ||||
|         itemCount: _tracks?.length ?? 3, | ||||
|         itemCount: tracks?.length ?? 3, | ||||
|         itemBuilder: (context, idx) { | ||||
|           final item = _tracks?[idx]; | ||||
|           return ListTile( | ||||
|             leading: ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: item != null | ||||
|                   ? AutoCacheImage( | ||||
|                       item.album!.images!.first.url!, | ||||
|                       width: 64.0, | ||||
|                       height: 64.0, | ||||
|                     ) | ||||
|                   : const SizedBox( | ||||
|                       width: 64, | ||||
|                       height: 64, | ||||
|                       child: Center( | ||||
|                         child: Icon(Icons.image), | ||||
|                       ), | ||||
|                     ), | ||||
|             ), | ||||
|             title: Text(item?.name ?? 'Loading...'), | ||||
|             subtitle: Text( | ||||
|               item?.artists?.asString() ?? 'Please stand by...', | ||||
|               maxLines: 1, | ||||
|               overflow: TextOverflow.ellipsis, | ||||
|             ), | ||||
|           final item = tracks?[idx]; | ||||
|           return TrackTile( | ||||
|             item: item, | ||||
|             onTap: () { | ||||
|               if (item == null) return; | ||||
|               Get.find<AudioPlayerProvider>() | ||||
|                 ..load( | ||||
|                   _tracks!, | ||||
|                   tracks!, | ||||
|                   initialIndex: idx, | ||||
|                   autoPlay: true, | ||||
|                 ) | ||||
|                 ..addCollection(widget.playlistId); | ||||
|                 ..addCollection(playlistId); | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:rhythm_box/providers/audio_player.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:rhythm_box/widgets/tracks/track_tile.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
| import 'package:rhythm_box/services/artist.dart'; | ||||
|  | ||||
| class TrackSliverList extends StatelessWidget { | ||||
|   final List<Track> tracks; | ||||
| @@ -19,21 +18,8 @@ class TrackSliverList extends StatelessWidget { | ||||
|       itemCount: tracks.length, | ||||
|       itemBuilder: (context, idx) { | ||||
|         final item = tracks[idx]; | ||||
|         return ListTile( | ||||
|           leading: ClipRRect( | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|             child: AutoCacheImage( | ||||
|               item.album!.images!.first.url!, | ||||
|               width: 64.0, | ||||
|               height: 64.0, | ||||
|             ), | ||||
|           ), | ||||
|           title: Text(item.name ?? 'Loading...'), | ||||
|           subtitle: Text( | ||||
|             item.artists?.asString() ?? 'Please stand by...', | ||||
|             maxLines: 1, | ||||
|             overflow: TextOverflow.ellipsis, | ||||
|           ), | ||||
|         return TrackTile( | ||||
|           item: item, | ||||
|           onTap: () { | ||||
|             Get.find<AudioPlayerProvider>().load( | ||||
|               [item], | ||||
|   | ||||
							
								
								
									
										44
									
								
								lib/widgets/tracks/track_tile.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								lib/widgets/tracks/track_tile.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
| import 'package:rhythm_box/services/artist.dart'; | ||||
|  | ||||
| class TrackTile extends StatelessWidget { | ||||
|   final Track? item; | ||||
|  | ||||
|   final Function? onTap; | ||||
|  | ||||
|   const TrackTile({super.key, required this.item, this.onTap}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListTile( | ||||
|       leading: ClipRRect( | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         child: (item?.album?.images?.isNotEmpty ?? false) | ||||
|             ? AutoCacheImage( | ||||
|                 item!.album!.images!.first.url!, | ||||
|                 width: 64.0, | ||||
|                 height: 64.0, | ||||
|               ) | ||||
|             : const SizedBox( | ||||
|                 width: 64, | ||||
|                 height: 64, | ||||
|                 child: Center( | ||||
|                   child: Icon(Icons.image), | ||||
|                 ), | ||||
|               ), | ||||
|       ), | ||||
|       title: Text(item?.name ?? 'Loading...'), | ||||
|       subtitle: Text( | ||||
|         item?.artists?.asString() ?? 'Please stand by...', | ||||
|         maxLines: 1, | ||||
|         overflow: TextOverflow.ellipsis, | ||||
|       ), | ||||
|       onTap: () { | ||||
|         if (onTap == null) return; | ||||
|         onTap!(); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user