diff --git a/lib/router.dart b/lib/router.dart index a3a7eda..5b7452b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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', diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index ea9a055..b387aa9 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -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 { 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( diff --git a/lib/screens/library/view.dart b/lib/screens/library/view.dart new file mode 100644 index 0000000..fec0ff0 --- /dev/null +++ b/lib/screens/library/view.dart @@ -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 createState() => _LibraryScreenState(); +} + +class _LibraryScreenState extends State { + 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()), + ], + ); + }), + ), + ); + } +} diff --git a/lib/screens/player/lyrics.dart b/lib/screens/player/lyrics.dart index bb3724c..e239138 100644 --- a/lib/screens/player/lyrics.dart +++ b/lib/screens/player/lyrics.dart @@ -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 { diff --git a/lib/screens/player/view.dart b/lib/screens/player/view.dart index 99a1177..3e0d6c0 100644 --- a/lib/screens/player/view.dart +++ b/lib/screens/player/view.dart @@ -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,45 +75,55 @@ class _PlayerScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - LimitedBox( - maxHeight: maxAlbumSize, - maxWidth: maxAlbumSize, - child: Hero( - tag: const Key('current-active-track-album-art'), - child: ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(16)), - child: AspectRatio( - aspectRatio: 1, - child: _albumArt != null - ? AutoCacheImage( - _albumArt!, - width: albumSize, - height: albumSize, - ) - : Container( - color: Theme.of(context) - .colorScheme - .surfaceContainerHigh, - width: 64, - height: 64, - child: - const Center(child: Icon(Icons.image)), - ), - ), - ).marginSymmetric(horizontal: 24), + Obx( + () => LimitedBox( + maxHeight: maxAlbumSize, + maxWidth: maxAlbumSize, + child: Hero( + tag: const Key('current-active-track-album-art'), + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(16)), + child: AspectRatio( + aspectRatio: 1, + child: _albumArt != null + ? AutoCacheImage( + _albumArt!, + width: albumSize, + height: albumSize, + ) + : Container( + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh, + width: 64, + height: 64, + 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, + Obx( + () => Text( + _playback.state.value.activeTrack?.name ?? + 'Not playing', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), ), - Text( - _playback.state.value.activeTrack?.artists?.asString() ?? - 'No author', - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, + 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,43 +207,49 @@ class _PlayerScreenState extends State { ); }, ), - IconButton( - icon: const Icon(Icons.skip_previous), - onPressed: _isFetchingActiveTrack - ? null - : audioPlayer.skipToPrevious, - ), - const Gap(8), - SizedBox( - width: 56, - height: 56, - child: IconButton.filled( - icon: _isFetchingActiveTrack - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.5, - ), - ) - : Icon( - !_isPlaying - ? Icons.play_arrow - : Icons.pause, - size: 28, - ), + Obx( + () => IconButton( + icon: const Icon(Icons.skip_previous), onPressed: _isFetchingActiveTrack ? null - : _togglePlayState, + : audioPlayer.skipToPrevious, ), ), const Gap(8), - IconButton( - icon: const Icon(Icons.skip_next), - onPressed: _isFetchingActiveTrack - ? null - : audioPlayer.skipToNext, + Obx( + () => SizedBox( + width: 56, + height: 56, + child: IconButton.filled( + icon: _isFetchingActiveTrack + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.5, + ), + ) + : Icon( + !_isPlaying + ? Icons.play_arrow + : Icons.pause, + size: 28, + ), + onPressed: _isFetchingActiveTrack + ? null + : _togglePlayState, + ), + ), + ), + const Gap(8), + Obx( + () => IconButton( + icon: const Icon(Icons.skip_next), + onPressed: _isFetchingActiveTrack + ? null + : audioPlayer.skipToNext, + ), ), Obx( () => IconButton( diff --git a/lib/screens/playlist/view.dart b/lib/screens/playlist/view.dart index d434034..f98d74f 100644 --- a/lib/screens/playlist/view.dart +++ b/lib/screens/playlist/view.dart @@ -37,19 +37,46 @@ class _PlaylistViewScreenState extends State { : false; bool _isLoading = true; + bool _isLoadingTracks = true; bool _isUpdating = false; Playlist? _playlist; + List? _tracks; Future _pullPlaylist() async { - _playlist = await _spotify.api.playlists.get(widget.playlistId); + 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 _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,14 +113,17 @@ class _PlaylistViewScreenState extends State { elevation: 2, child: ClipRRect( borderRadius: radius, - child: Hero( - tag: Key('playlist-cover-${_playlist!.id}'), - child: AutoCacheImage( - _playlist!.images!.first.url!, - width: 160.0, - height: 160.0, - ), - ), + 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), + ), ), ), const Gap(24), @@ -116,7 +146,7 @@ class _PlaylistViewScreenState extends State { ), 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 { 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() @@ -180,18 +203,11 @@ class _PlaylistViewScreenState extends State { 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() @@ -208,11 +224,15 @@ class _PlaylistViewScreenState extends State { ), 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, + ), ], ), ); diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 26ffdfa..b69d89b 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -45,8 +45,6 @@ class _SettingsScreenState extends State { return FutureBuilder( future: _spotify.api.me.get(), builder: (context, snapshot) { - print(snapshot.data); - print(snapshot.error); if (!snapshot.hasData) { return const ListTile( contentPadding: diff --git a/lib/shells/nav_shell.dart b/lib/shells/nav_shell.dart index d9244d4..7762b76 100644 --- a/lib/shells/nav_shell.dart +++ b/lib/shells/nav_shell.dart @@ -24,6 +24,7 @@ class _NavShellState extends State { final List _allDestinations = [ 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 { 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, diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index cf597c3..488598b 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -1,6 +1,7 @@ const i18nEnglish = { 'appName': 'RhythmBox', 'explore': 'Explore', + 'library': 'Library', 'settings': 'Settings', 'search': 'Search', }; diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index 6a3f035..b560dd1 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -1,6 +1,7 @@ const i18nSimplifiedChinese = { 'appName': '韵律盒', 'explore': '探索', + 'library': '资料库', 'settings': '设置', 'search': '搜索', }; diff --git a/lib/widgets/lyrics/synced.dart b/lib/widgets/lyrics/synced_lyrics.dart similarity index 87% rename from lib/widgets/lyrics/synced.dart rename to lib/widgets/lyrics/synced_lyrics.dart index cf180ea..e07a9ca 100644 --- a/lib/widgets/lyrics/synced.dart +++ b/lib/widgets/lyrics/synced_lyrics.dart @@ -28,15 +28,16 @@ class _SyncedLyricsState extends State { 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 _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 { @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 { 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 && - (lyricNextSlice == null || - lyricNextSlice.time.inSeconds > - _durationCurrent.inSeconds); + final isActive = _playback.durationCurrent.value.inSeconds >= + lyricSlice.time.inSeconds && + (lyricNextSlice == null || + lyricNextSlice.time.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 { ), ), ); - }, + }), ), ], ); diff --git a/lib/widgets/no_login_fallback.dart b/lib/widgets/no_login_fallback.dart new file mode 100644 index 0000000..36ab16b --- /dev/null +++ b/lib/widgets/no_login_fallback.dart @@ -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, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/playlist/playlist_tile.dart b/lib/widgets/playlist/playlist_tile.dart new file mode 100644 index 0000000..9cb543e --- /dev/null +++ b/lib/widgets/playlist/playlist_tile.dart @@ -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!(); + }, + ); + } +} diff --git a/lib/widgets/playlist/user_playlist_list.dart b/lib/widgets/playlist/user_playlist_list.dart new file mode 100644 index 0000000..5890e34 --- /dev/null +++ b/lib/widgets/playlist/user_playlist_list.dart @@ -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 createState() => _UserPlaylistListState(); +} + +class _UserPlaylistListState extends State { + 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? _playlist; + + Future _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!}, + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/widgets/tracks/playlist_track_list.dart b/lib/widgets/tracks/playlist_track_list.dart index 41f0598..da7ffa2 100644 --- a/lib/widgets/tracks/playlist_track_list.dart +++ b/lib/widgets/tracks/playlist_track_list.dart @@ -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? tracks; - const PlaylistTrackList({super.key, required this.playlistId}); + final bool isLoading; - @override - State createState() => _PlaylistTrackListState(); -} - -class _PlaylistTrackListState extends State { - late final SpotifyProvider _spotify = Get.find(); - - bool _isLoading = true; - - List? _tracks; - - Future _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() ..load( - _tracks!, + tracks!, initialIndex: idx, autoPlay: true, ) - ..addCollection(widget.playlistId); + ..addCollection(playlistId); }, ); }, diff --git a/lib/widgets/tracks/track_list.dart b/lib/widgets/tracks/track_list.dart index 032437a..ad7c23a 100644 --- a/lib/widgets/tracks/track_list.dart +++ b/lib/widgets/tracks/track_list.dart @@ -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 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().load( [item], diff --git a/lib/widgets/tracks/track_tile.dart b/lib/widgets/tracks/track_tile.dart new file mode 100644 index 0000000..fbbc090 --- /dev/null +++ b/lib/widgets/tracks/track_tile.dart @@ -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!(); + }, + ); + } +}