✨ Bottom player
This commit is contained in:
		| @@ -28,6 +28,8 @@ android { | ||||
|         targetSdk = flutter.targetSdkVersion | ||||
|         versionCode = flutter.versionCode | ||||
|         versionName = flutter.versionName | ||||
|  | ||||
|         minSdkVersion 24 | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|   | ||||
							
								
								
									
										3
									
								
								devtools_options.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								devtools_options.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| description: This file stores settings for Dart & Flutter DevTools. | ||||
| documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states | ||||
| extensions: | ||||
| @@ -9,6 +9,7 @@ import 'package:rhythm_box/services/server/routes/playback.dart'; | ||||
| import 'package:rhythm_box/services/server/server.dart'; | ||||
| import 'package:rhythm_box/services/server/sourced_track.dart'; | ||||
| import 'package:rhythm_box/translations.dart'; | ||||
| import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; | ||||
|  | ||||
| void main() { | ||||
|   MediaKit.ensureInitialized(); | ||||
| @@ -51,9 +52,11 @@ class MyApp extends StatelessWidget { | ||||
|   void _initializeProviders(BuildContext context) async { | ||||
|     Get.lazyPut(() => SpotifyProvider()); | ||||
|     Get.lazyPut(() => ActiveSourcedTrackProvider()); | ||||
|     Get.lazyPut(() => SourcedTrackProvider()); | ||||
|  | ||||
|     Get.put(AudioPlayerProvider()); | ||||
|     Get.put(QueryingTrackInfoProvider()); | ||||
|     Get.put(SourcedTrackProvider()); | ||||
|  | ||||
|     Get.put(ServerPlaybackRoutesProvider()); | ||||
|     Get.put(PlaybackServerProvider()); | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:get/get.dart'; | ||||
| @@ -13,6 +14,8 @@ import 'package:shared_preferences/shared_preferences.dart'; | ||||
| class AudioPlayerProvider extends GetxController { | ||||
|   late final SharedPreferences _prefs; | ||||
|  | ||||
|   RxBool isPlaying = false.obs; | ||||
|  | ||||
|   Rx<AudioPlayerState> state = Rx(AudioPlayerState( | ||||
|     playing: false, | ||||
|     shuffled: false, | ||||
| @@ -25,33 +28,44 @@ class AudioPlayerProvider extends GetxController { | ||||
|  | ||||
|   @override | ||||
|   void onInit() { | ||||
|     SharedPreferences.getInstance().then((ins) { | ||||
|     SharedPreferences.getInstance().then((ins) async { | ||||
|       _prefs = ins; | ||||
|       _syncSavedState(); | ||||
|       final res = await _readSavedState(); | ||||
|       if (res != null) { | ||||
|         state.value = res; | ||||
|       } else { | ||||
|         state.value = AudioPlayerState( | ||||
|           loopMode: audioPlayer.loopMode, | ||||
|           playing: audioPlayer.isPlaying, | ||||
|           playlist: audioPlayer.playlist, | ||||
|           shuffled: audioPlayer.isShuffled, | ||||
|           collections: [], | ||||
|         ); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     _subscriptions = [ | ||||
|       audioPlayer.playingStream.listen((playing) async { | ||||
|         state.value = state.value.copyWith(playing: playing); | ||||
|         await _updateSavedState(); | ||||
|       }), | ||||
|       audioPlayer.loopModeStream.listen((loopMode) async { | ||||
|         state.value = state.value.copyWith(loopMode: loopMode); | ||||
|         await _updateSavedState(); | ||||
|       }), | ||||
|       audioPlayer.shuffledStream.listen((shuffled) async { | ||||
|         state.value = state.value.copyWith(shuffled: shuffled); | ||||
|         await _updateSavedState(); | ||||
|       }), | ||||
|       audioPlayer.playlistStream.listen((playlist) async { | ||||
|         state.value = state.value.copyWith(playlist: playlist); | ||||
|         await _updateSavedState(); | ||||
|       }), | ||||
|     ]; | ||||
|  | ||||
|     state.value = AudioPlayerState( | ||||
|       loopMode: audioPlayer.loopMode, | ||||
|       playing: audioPlayer.isPlaying, | ||||
|       playlist: audioPlayer.playlist, | ||||
|       shuffled: audioPlayer.isShuffled, | ||||
|       collections: [], | ||||
|     ); | ||||
|     audioPlayer.playingStream.listen((playing) { | ||||
|       isPlaying.value = playing; | ||||
|     }); | ||||
|  | ||||
|     super.onInit(); | ||||
|   } | ||||
| @@ -66,13 +80,16 @@ class AudioPlayerProvider extends GetxController { | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   Future<void> _syncSavedState() async { | ||||
|     final data = _prefs.getBool("player_state"); | ||||
|     if (data == null) return; | ||||
|   Future<AudioPlayerState?> _readSavedState() async { | ||||
|     final data = _prefs.getString("player_state"); | ||||
|     if (data == null) return null; | ||||
|  | ||||
|     // TODO Serilize and deserilize this state | ||||
|     return AudioPlayerState.fromJson(jsonDecode(data)); | ||||
|   } | ||||
|  | ||||
|     // TODO Sync saved playlist | ||||
|   Future<void> _updateSavedState() async { | ||||
|     final out = jsonEncode(state.value.toJson()); | ||||
|     await _prefs.setString("player_state", out); | ||||
|   } | ||||
|  | ||||
|   Future<void> addCollections(List<String> collectionIds) async { | ||||
| @@ -80,6 +97,8 @@ class AudioPlayerProvider extends GetxController { | ||||
|       ...state.value.collections, | ||||
|       ...collectionIds, | ||||
|     ]); | ||||
|  | ||||
|     await _updateSavedState(); | ||||
|   } | ||||
|  | ||||
|   Future<void> addCollection(String collectionId) async { | ||||
| @@ -92,6 +111,8 @@ class AudioPlayerProvider extends GetxController { | ||||
|           .where((element) => !collectionIds.contains(element)) | ||||
|           .toList(), | ||||
|     ); | ||||
|  | ||||
|     await _updateSavedState(); | ||||
|   } | ||||
|  | ||||
|   Future<void> removeCollection(String collectionId) async { | ||||
|   | ||||
| @@ -13,6 +13,13 @@ final router = GoRouter(routes: [ | ||||
|         name: "explore", | ||||
|         builder: (context, state) => const ExploreScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: "/playlist/:id", | ||||
|         name: "playlistView", | ||||
|         builder: (context, state) => PlaylistViewScreen( | ||||
|           playlistId: state.pathParameters['id']!, | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: "/settings", | ||||
|         name: "settings", | ||||
| @@ -20,11 +27,4 @@ final router = GoRouter(routes: [ | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: "/playlist/:id", | ||||
|     name: "playlistView", | ||||
|     builder: (context, state) => PlaylistViewScreen( | ||||
|       playlistId: state.pathParameters['id']!, | ||||
|     ), | ||||
|   ), | ||||
| ]); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|  | ||||
|   Future<void> _pullPlaylist() async { | ||||
|     _featuredPlaylist = | ||||
|         (await _spotify.api.playlists.featured.all(10)).toList(); | ||||
|         (await _spotify.api.playlists.featured.getPage(20)).items!.toList(); | ||||
|     setState(() => _isLoading = false); | ||||
|   } | ||||
|  | ||||
| @@ -48,7 +48,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|               final item = _featuredPlaylist?[idx]; | ||||
|               return ListTile( | ||||
|                 leading: ClipRRect( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   child: item != null | ||||
|                       ? AutoCacheImage( | ||||
|                           item.images!.first.url!, | ||||
|   | ||||
| @@ -1,6 +1,12 @@ | ||||
| import 'package:spotify/spotify.dart'; | ||||
|  | ||||
| extension ArtistExtension on List<ArtistSimple> { | ||||
| extension ArtistSimpleExtension on List<ArtistSimple> { | ||||
|   String asString() { | ||||
|     return map((e) => e.name?.replaceAll(",", " ")).join(", "); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension ArtistExtension on List<Artist> { | ||||
|   String asString() { | ||||
|     return map((e) => e.name?.replaceAll(",", " ")).join(", "); | ||||
|   } | ||||
|   | ||||
| @@ -48,11 +48,11 @@ class AudioServices with WidgetsBindingObserver { | ||||
|       duration: track is SourcedTrack | ||||
|           ? track.sourceInfo.duration | ||||
|           : Duration(milliseconds: track.durationMs ?? 0), | ||||
|       artUri: Uri.parse( | ||||
|         (track.album?.images).asUrlString( | ||||
|           placeholder: ImagePlaceholder.albumArt, | ||||
|         ), | ||||
|       ), | ||||
|       artUri: track.album?.images != null | ||||
|           ? Uri.parse( | ||||
|               (track.album?.images).asUrlString()!, | ||||
|             ) | ||||
|           : null, | ||||
|       playable: true, | ||||
|     )); | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| import 'package:rhythm_box/services/primitive.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
| import 'package:rhythm_box/collections/assets.gen.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
|  | ||||
| enum ImagePlaceholder { | ||||
| @@ -11,24 +9,13 @@ enum ImagePlaceholder { | ||||
| } | ||||
|  | ||||
| extension SpotifyImageExtensions on List<Image>? { | ||||
|   String asUrlString({ | ||||
|   String? asUrlString({ | ||||
|     int index = 1, | ||||
|     required ImagePlaceholder placeholder, | ||||
|   }) { | ||||
|     final String placeholderUrl = { | ||||
|       ImagePlaceholder.albumArt: Assets.albumPlaceholder.path, | ||||
|       ImagePlaceholder.artist: Assets.userPlaceholder.path, | ||||
|       ImagePlaceholder.collection: Assets.placeholder.path, | ||||
|       ImagePlaceholder.online: | ||||
|           "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png", | ||||
|     }[placeholder]!; | ||||
|  | ||||
|     final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!)); | ||||
|  | ||||
|     return sortedImage != null && sortedImage.isNotEmpty | ||||
|         ? sortedImage[ | ||||
|                 index > sortedImage.length - 1 ? sortedImage.length - 1 : index] | ||||
|             .url! | ||||
|         : placeholderUrl; | ||||
|     return sortedImage?[ | ||||
|             index > sortedImage.length - 1 ? sortedImage.length - 1 : index] | ||||
|         .url; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -84,9 +84,7 @@ class WindowsAudioService { | ||||
|         albumArtist: track.artists?.firstOrNull?.name ?? "Unknown", | ||||
|         artist: track.artists?.asString() ?? "Unknown", | ||||
|         album: track.album?.name ?? "Unknown", | ||||
|         thumbnail: (track.album?.images).asUrlString( | ||||
|           placeholder: ImagePlaceholder.albumArt, | ||||
|         ), | ||||
|         thumbnail: (track.album?.images).asUrlString(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:rhythm_box/providers/audio_player.dart'; | ||||
| import 'package:rhythm_box/services/audio_player/audio_player.dart'; | ||||
| import 'package:rhythm_box/services/local_track.dart'; | ||||
| import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; | ||||
| import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
|  | ||||
| class SourcedTrackProvider extends GetxController { | ||||
| @@ -17,6 +18,7 @@ class SourcedTrackProvider extends GetxController { | ||||
|     } | ||||
|  | ||||
|     final AudioPlayerProvider playback = Get.find(); | ||||
|     final QueryingTrackInfoProvider query = Get.find(); | ||||
|  | ||||
|     ever(playback.state.value.tracks.obs, (List<Track> tracks) { | ||||
|       if (tracks.isEmpty || tracks.none((element) => element.id == track.id)) { | ||||
| @@ -24,7 +26,9 @@ class SourcedTrackProvider extends GetxController { | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     query.isQueryingTrackInfo.value = true; | ||||
|     sourcedTrack.value = await SourcedTrack.fetchFromTrack(track: track); | ||||
|     query.isQueryingTrackInfo.value = false; | ||||
|  | ||||
|     return sourcedTrack.value; | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:rhythm_box/widgets/player/bottom_player.dart'; | ||||
|  | ||||
| class Destination { | ||||
|   const Destination(this.title, this.page, this.icon); | ||||
| @@ -30,19 +31,30 @@ class _NavShellState extends State<NavShell> { | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       body: widget.child, | ||||
|       bottomNavigationBar: BottomNavigationBar( | ||||
|         showUnselectedLabels: false, | ||||
|         currentIndex: _focusDestination, | ||||
|         items: _allDestinations | ||||
|             .map((x) => BottomNavigationBarItem( | ||||
|                   icon: Icon(x.icon), | ||||
|                   label: x.title, | ||||
|                 )) | ||||
|             .toList(), | ||||
|         onTap: (value) { | ||||
|           GoRouter.of(context).goNamed(_allDestinations[value].page); | ||||
|           setState(() => _focusDestination = value); | ||||
|         }, | ||||
|       bottomNavigationBar: Material( | ||||
|         elevation: 2, | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             const BottomPlayer(), | ||||
|             const Divider(height: 0.3, thickness: 0.3), | ||||
|             BottomNavigationBar( | ||||
|               elevation: 0, | ||||
|               showUnselectedLabels: false, | ||||
|               currentIndex: _focusDestination, | ||||
|               items: _allDestinations | ||||
|                   .map((x) => BottomNavigationBarItem( | ||||
|                         icon: Icon(x.icon), | ||||
|                         label: x.title, | ||||
|                       )) | ||||
|                   .toList(), | ||||
|               onTap: (value) { | ||||
|                 GoRouter.of(context).goNamed(_allDestinations[value].page); | ||||
|                 setState(() => _focusDestination = value); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										172
									
								
								lib/widgets/player/bottom_player.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								lib/widgets/player/bottom_player.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:math'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:rhythm_box/providers/audio_player.dart'; | ||||
| import 'package:rhythm_box/services/audio_player/audio_player.dart'; | ||||
| import 'package:rhythm_box/services/audio_services/image.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:rhythm_box/widgets/player/track_details.dart'; | ||||
| import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; | ||||
|  | ||||
| class BottomPlayer extends StatefulWidget { | ||||
|   const BottomPlayer({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<BottomPlayer> createState() => _BottomPlayerState(); | ||||
| } | ||||
|  | ||||
| class _BottomPlayerState extends State<BottomPlayer> | ||||
|     with SingleTickerProviderStateMixin { | ||||
|   late final AnimationController _animationController = AnimationController( | ||||
|     duration: const Duration(milliseconds: 500), | ||||
|     vsync: this, | ||||
|   ); | ||||
|   late final Animation<double> _animation = CurvedAnimation( | ||||
|     parent: _animationController, | ||||
|     curve: Curves.easeInOut, | ||||
|   ); | ||||
|  | ||||
|   late final AudioPlayerProvider _playback = Get.find(); | ||||
|   late final QueryingTrackInfoProvider _query = Get.find(); | ||||
|  | ||||
|   String? get _albumArt => | ||||
|       (_playback.state.value.activeTrack?.album?.images).asUrlString( | ||||
|         index: | ||||
|             (_playback.state.value.activeTrack?.album?.images?.length ?? 1) - 1, | ||||
|       ); | ||||
|  | ||||
|   bool get _isPlaying => _playback.isPlaying.value; | ||||
|   bool get _isFetchingActiveTrack => _query.isQueryingTrackInfo.value; | ||||
|  | ||||
|   Duration _durationCurrent = Duration.zero; | ||||
|   Duration _durationTotal = Duration.zero; | ||||
|  | ||||
|   void _updateDurationCurrent(Duration dur) { | ||||
|     setState(() => _durationCurrent = dur); | ||||
|   } | ||||
|  | ||||
|   void _updateDurationTotal(Duration dur) { | ||||
|     setState(() => _durationTotal = dur); | ||||
|   } | ||||
|  | ||||
|   List<StreamSubscription>? _subscriptions; | ||||
|  | ||||
|   Future<void> _togglePlayState() async { | ||||
|     if (!audioPlayer.isPlaying) { | ||||
|       await audioPlayer.resume(); | ||||
|     } else { | ||||
|       await audioPlayer.pause(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool _isLifted = false; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _subscriptions = [ | ||||
|       audioPlayer.durationStream.listen(_updateDurationTotal), | ||||
|       audioPlayer.positionStream.listen(_updateDurationCurrent), | ||||
|       _playback.isPlaying.listen((value) { | ||||
|         if (value && !_isLifted) { | ||||
|           _animationController.animateTo(1); | ||||
|           _isLifted = true; | ||||
|         } | ||||
|       }), | ||||
|       _query.isQueryingTrackInfo.listen((value) { | ||||
|         if (value && !_isLifted) { | ||||
|           _animationController.animateTo(1); | ||||
|           _isLifted = true; | ||||
|         } | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     if (_subscriptions != null) { | ||||
|       for (final subscription in _subscriptions!) { | ||||
|         subscription.cancel(); | ||||
|       } | ||||
|     } | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SizeTransition( | ||||
|       sizeFactor: _animation, | ||||
|       axis: Axis.vertical, | ||||
|       axisAlignment: -1, | ||||
|       child: Obx( | ||||
|         () => Column( | ||||
|           children: [ | ||||
|             if (_durationCurrent != Duration.zero) | ||||
|               TweenAnimationBuilder<double>( | ||||
|                 tween: Tween( | ||||
|                   begin: 0, | ||||
|                   end: _durationCurrent.inMilliseconds / | ||||
|                       max(_durationTotal.inMilliseconds, 1), | ||||
|                 ), | ||||
|                 duration: const Duration(milliseconds: 100), | ||||
|                 builder: (context, value, _) => LinearProgressIndicator( | ||||
|                   minHeight: 3, | ||||
|                   value: value, | ||||
|                 ), | ||||
|               ), | ||||
|             Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|               children: [ | ||||
|                 ClipRRect( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   child: _albumArt != null | ||||
|                       ? AutoCacheImage(_albumArt!, width: 64, height: 64) | ||||
|                       : Container( | ||||
|                           color: Theme.of(context) | ||||
|                               .colorScheme | ||||
|                               .surfaceContainerHigh, | ||||
|                           width: 64, | ||||
|                           height: 64, | ||||
|                           child: const Center(child: Icon(Icons.image)), | ||||
|                         ), | ||||
|                 ), | ||||
|                 const Gap(12), | ||||
|                 Expanded( | ||||
|                   child: PlayerTrackDetails( | ||||
|                     track: _playback.state.value.activeTrack, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const Gap(12), | ||||
|                 Row( | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   mainAxisAlignment: MainAxisAlignment.end, | ||||
|                   children: [ | ||||
|                     IconButton( | ||||
|                       icon: _isFetchingActiveTrack | ||||
|                           ? const SizedBox( | ||||
|                               height: 20, | ||||
|                               width: 20, | ||||
|                               child: CircularProgressIndicator( | ||||
|                                 strokeWidth: 3, | ||||
|                               ), | ||||
|                             ) | ||||
|                           : Icon( | ||||
|                               !_isPlaying ? Icons.play_arrow : Icons.pause, | ||||
|                             ), | ||||
|                       onPressed: | ||||
|                           _isFetchingActiveTrack ? null : _togglePlayState, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 const Gap(12), | ||||
|               ], | ||||
|             ).paddingSymmetric(horizontal: 12, vertical: 8), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										49
									
								
								lib/widgets/player/track_details.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/widgets/player/track_details.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:rhythm_box/providers/audio_player.dart'; | ||||
| import 'package:rhythm_box/services/artist.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
|  | ||||
| class PlayerTrackDetails extends StatelessWidget { | ||||
|   final Color? color; | ||||
|   final Track? track; | ||||
|   const PlayerTrackDetails({super.key, this.color, this.track}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final theme = Theme.of(context); | ||||
|     final AudioPlayerProvider playback = Get.find(); | ||||
|  | ||||
|     return Row( | ||||
|       children: [ | ||||
|         Flexible( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               const SizedBox(height: 4), | ||||
|               InkWell( | ||||
|                 child: Text( | ||||
|                   playback.state.value.activeTrack?.name ?? "Not playing", | ||||
|                   maxLines: 1, | ||||
|                   overflow: TextOverflow.ellipsis, | ||||
|                   style: theme.textTheme.bodyMedium!.copyWith( | ||||
|                     color: color, | ||||
|                   ), | ||||
|                 ), | ||||
|                 onTap: () { | ||||
|                   // TODO Push to track page | ||||
|                 }, | ||||
|               ), | ||||
|               Text( | ||||
|                 playback.state.value.activeTrack?.artists?.asString() ?? | ||||
|                     "No author", | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|                 style: theme.textTheme.bodySmall!.copyWith(color: color), | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -5,6 +5,7 @@ import 'package:rhythm_box/providers/spotify.dart'; | ||||
| import 'package:rhythm_box/widgets/auto_cache_image.dart'; | ||||
| import 'package:skeletonizer/skeletonizer.dart'; | ||||
| import 'package:spotify/spotify.dart'; | ||||
| import 'package:rhythm_box/services/artist.dart'; | ||||
|  | ||||
| class PlaylistTrackList extends StatefulWidget { | ||||
|   final String playlistId; | ||||
| @@ -46,7 +47,7 @@ class _PlaylistTrackListState extends State<PlaylistTrackList> { | ||||
|           final item = _tracks?[idx]; | ||||
|           return ListTile( | ||||
|             leading: ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: item != null | ||||
|                   ? AutoCacheImage( | ||||
|                       item.album!.images!.first.url!, | ||||
| @@ -63,8 +64,7 @@ class _PlaylistTrackListState extends State<PlaylistTrackList> { | ||||
|             ), | ||||
|             title: Text(item?.name ?? 'Loading...'), | ||||
|             subtitle: Text( | ||||
|               item?.artists!.map((x) => x.name!).join(', ') ?? | ||||
|                   'Please stand by...', | ||||
|               item?.artists?.asString() ?? 'Please stand by...', | ||||
|             ), | ||||
|             onTap: () { | ||||
|               if (item == null) return; | ||||
|   | ||||
							
								
								
									
										5
									
								
								lib/widgets/tracks/querying_track_info.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/widgets/tracks/querying_track_info.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import 'package:get/get.dart'; | ||||
|  | ||||
| class QueryingTrackInfoProvider extends GetxController { | ||||
|   RxBool isQueryingTrackInfo = false.obs; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user