diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 3c4a409b..56dce006 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -63,11 +63,9 @@ class UserAlbums extends HookConsumerWidget { }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), - child: Material( - type: MaterialType.transparency, - color: Theme.of(context).scaffoldBackgroundColor, - child: Padding( - padding: const EdgeInsets.all(8.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SafeArea( child: Column( children: [ TextField( diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index ccabb0c6..76e2a443 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -84,31 +84,34 @@ class UserArtists extends HookConsumerWidget { onRefresh: () async { await artistQuery.refreshAll(); }, - child: GridView.builder( - itemCount: filteredArtists.length, - physics: const AlwaysScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: 250, - crossAxisSpacing: 20, - mainAxisSpacing: 20, + child: SafeArea( + child: GridView.builder( + itemCount: filteredArtists.length, + physics: const AlwaysScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: 250, + crossAxisSpacing: 20, + mainAxisSpacing: 20, + ), + padding: const EdgeInsets.all(10), + itemBuilder: (context, index) { + return HookBuilder(builder: (context) { + if (index == artistQuery.pages.length - 1 && + hasNextPage) { + return Waypoint( + controller: useScrollController(), + isGrid: true, + onTouchEdge: () { + artistQuery.fetchNext(); + }, + child: ArtistCard(filteredArtists[index]), + ); + } + return ArtistCard(filteredArtists[index]); + }); + }, ), - padding: const EdgeInsets.all(10), - itemBuilder: (context, index) { - return HookBuilder(builder: (context) { - if (index == artistQuery.pages.length - 1 && hasNextPage) { - return Waypoint( - controller: useScrollController(), - isGrid: true, - onTouchEdge: () { - artistQuery.fetchNext(); - }, - child: ArtistCard(filteredArtists[index]), - ); - } - return ArtistCard(filteredArtists[index]); - }); - }, ), ), ); diff --git a/lib/components/library/user_downloads.dart b/lib/components/library/user_downloads.dart index 44249e93..a5af4c2a 100644 --- a/lib/components/library/user_downloads.dart +++ b/lib/components/library/user_downloads.dart @@ -44,38 +44,40 @@ class UserDownloads extends HookConsumerWidget { ), ), Expanded( - child: ListView.builder( - itemCount: downloader.inQueue.length, - itemBuilder: (context, index) { - final track = downloader.inQueue.elementAt(index); - return ListTile( - title: Text(track.name ?? ''), - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - height: 40, - width: 40, - path: TypeConversionUtils.image_X_UrlString( - track.album?.images, - placeholder: ImagePlaceholder.albumArt, + child: SafeArea( + child: ListView.builder( + itemCount: downloader.inQueue.length, + itemBuilder: (context, index) { + final track = downloader.inQueue.elementAt(index); + return ListTile( + title: Text(track.name ?? ''), + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + height: 40, + width: 40, + path: TypeConversionUtils.image_X_UrlString( + track.album?.images, + placeholder: ImagePlaceholder.albumArt, + ), ), ), ), - ), - trailing: const SizedBox( - width: 30, - height: 30, - child: CircularProgressIndicator(), - ), - subtitle: Text( - TypeConversionUtils.artists_X_String( - track.artists ?? [], + trailing: const SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator(), ), - ), - ); - }, + subtitle: Text( + TypeConversionUtils.artists_X_String( + track.artists ?? [], + ), + ), + ); + }, + ), ), ), ], diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 80137964..5e43e4e4 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -92,10 +92,9 @@ class UserPlaylists extends HookConsumerWidget { onRefresh: playlistsQuery.refresh, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), - child: Material( - type: MaterialType.transparency, - child: Padding( - padding: const EdgeInsets.all(8.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SafeArea( child: Column( children: [ TextField( diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index d115582d..aa3fcf5f 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -260,8 +260,12 @@ class TracksTableView extends HookConsumerWidget { ]; if (isSliver) { - return SliverList(delegate: SliverChildListDelegate(children)); + return SliverSafeArea( + sliver: SliverList(delegate: SliverChildListDelegate(children)), + ); } - return ListView(children: children); + return SafeArea( + child: ListView(children: children), + ); } } diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 5be33a4b..e00108a6 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -85,325 +85,338 @@ class ArtistPage extends HookConsumerWidget { return SingleChildScrollView( controller: parentScrollController, padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - const SizedBox(width: 50), - CircleAvatar( - radius: avatarWidth, - backgroundImage: UniversalImage.imageProvider( - TypeConversionUtils.image_X_UrlString( - data.images, - placeholder: ImagePlaceholder.artist, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + const SizedBox(width: 50), + CircleAvatar( + radius: avatarWidth, + backgroundImage: UniversalImage.imageProvider( + TypeConversionUtils.image_X_UrlString( + data.images, + placeholder: ImagePlaceholder.artist, + ), ), ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(50)), - child: Text( - data.type!.toUpperCase(), - style: chipTextVariant?.copyWith( - color: Colors.white, - ), - ), - ), - if (isBlackListed) ...[ - const SizedBox(width: 5), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: Colors.red[400], + color: Colors.blue, borderRadius: BorderRadius.circular(50)), child: Text( - "Blacklisted", + data.type!.toUpperCase(), style: chipTextVariant?.copyWith( color: Colors.white, ), ), ), - ] - ], - ), - Text( - data.name!, - style: breakpoint.isSm - ? textTheme.headlineSmall - : textTheme.headlineMedium, - ), - Text( - "${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers", - style: textTheme.bodyMedium?.copyWith( - fontWeight: - breakpoint.isSm ? null : FontWeight.bold, + if (isBlackListed) ...[ + const SizedBox(width: 5), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.red[400], + borderRadius: + BorderRadius.circular(50)), + child: Text( + "Blacklisted", + style: chipTextVariant?.copyWith( + color: Colors.white, + ), + ), + ), + ] + ], ), - ), - const SizedBox(height: 20), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (auth != null) - HookBuilder( - builder: (context) { - final isFollowingQuery = useQueries.artist - .doIFollow(ref, artistId); + Text( + data.name!, + style: breakpoint.isSm + ? textTheme.headlineSmall + : textTheme.headlineMedium, + ), + Text( + "${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers", + style: textTheme.bodyMedium?.copyWith( + fontWeight: + breakpoint.isSm ? null : FontWeight.bold, + ), + ), + const SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (auth != null) + HookBuilder( + builder: (context) { + final isFollowingQuery = useQueries + .artist + .doIFollow(ref, artistId); - if (isFollowingQuery.isLoading || - !isFollowingQuery.hasData) { - return const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ); - } + if (isFollowingQuery.isLoading || + !isFollowingQuery.hasData) { + return const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ); + } - final queryBowl = QueryClient.of(context); + final queryBowl = + QueryClient.of(context); - return FilledButton( - onPressed: () async { - try { + return FilledButton( + onPressed: () async { + try { + isFollowingQuery.data! + ? await spotify.me.unfollow( + FollowingType.artist, + [artistId], + ) + : await spotify.me.follow( + FollowingType.artist, + [artistId], + ); + await isFollowingQuery.refresh(); + + queryBowl + .refreshInfiniteQueryAllPages( + "user-following-artists"); + } finally { + QueryClient.of(context).refreshQuery( + "user-follows-artists-query/$artistId"); + } + }, + child: Text( isFollowingQuery.data! - ? await spotify.me.unfollow( - FollowingType.artist, - [artistId], - ) - : await spotify.me.follow( - FollowingType.artist, - [artistId], - ); - await isFollowingQuery.refresh(); + ? "Following" + : "Follow", + ), + ); + }, + ), + const SizedBox(width: 5), + IconButton( + tooltip: "Add to blacklisted artists", + icon: Icon( + SpotubeIcons.userRemove, + color: !isBlackListed + ? Colors.red[400] + : Colors.white, + ), + style: IconButton.styleFrom( + backgroundColor: isBlackListed + ? Colors.red[400] + : null, + ), + onPressed: () async { + if (isBlackListed) { + ref + .read(BlackListNotifier + .provider.notifier) + .remove( + BlacklistedElement.artist( + data.id!, data.name!), + ); + } else { + ref + .read(BlackListNotifier + .provider.notifier) + .add( + BlacklistedElement.artist( + data.id!, data.name!), + ); + } + }, + ), + IconButton( + icon: const Icon(SpotubeIcons.share), + onPressed: () async { + await Clipboard.setData( + ClipboardData( + text: data.externalUrls?.spotify), + ); - queryBowl - .refreshInfiniteQueryAllPages( - "user-following-artists"); - } finally { - QueryClient.of(context).refreshQuery( - "user-follows-artists-query/$artistId"); - } - }, - child: Text( - isFollowingQuery.data! - ? "Following" - : "Follow", + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + "Artist URL copied to clipboard", + textAlign: TextAlign.center, + ), ), ); }, - ), - const SizedBox(width: 5), - IconButton( - tooltip: "Add to blacklisted artists", - icon: Icon( - SpotubeIcons.userRemove, - color: !isBlackListed - ? Colors.red[400] - : Colors.white, - ), - style: IconButton.styleFrom( - backgroundColor: - isBlackListed ? Colors.red[400] : null, - ), - onPressed: () async { - if (isBlackListed) { - ref - .read(BlackListNotifier - .provider.notifier) - .remove( - BlacklistedElement.artist( - data.id!, data.name!), - ); - } else { - ref - .read(BlackListNotifier - .provider.notifier) - .add( - BlacklistedElement.artist( - data.id!, data.name!), - ); - } - }, - ), - IconButton( - icon: const Icon(SpotubeIcons.share), - onPressed: () async { - await Clipboard.setData( - ClipboardData( - text: data.externalUrls?.spotify), - ); + ) + ], + ) + ], + ), + ), + ], + ), + const SizedBox(height: 50), + HookBuilder( + builder: (context) { + final topTracksQuery = useQueries.artist.topTracksOf( + ref, + artistId, + ); + final isPlaylistPlaying = + playlistNotifier.isPlayingPlaylist( + topTracksQuery.data ?? [], + ); + + if (topTracksQuery.isLoading || + !topTracksQuery.hasData) { + return const CircularProgressIndicator(); + } else if (topTracksQuery.hasError) { + return Center( + child: Text(topTracksQuery.error.toString()), + ); + } + + final topTracks = topTracksQuery.data!; + + void playPlaylist(List tracks, + {Track? currentTrack}) async { + currentTrack ??= tracks.first; + if (!isPlaylistPlaying) { + playlistNotifier.loadAndPlay(tracks, + active: tracks.indexWhere( + (s) => s.id == currentTrack?.id)); + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != playlist?.activeTrack.id) { + await playlistNotifier.playTrack(currentTrack); + } + } + + return Column(children: [ + Row( + children: [ + Text( + "Top Tracks", + style: + Theme.of(context).textTheme.headlineSmall, + ), + if (!isPlaylistPlaying) + IconButton( + icon: const Icon( + SpotubeIcons.queueAdd, + ), + onPressed: () { + playlistNotifier.add(topTracks.toList()); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( + SnackBar( width: 300, behavior: SnackBarBehavior.floating, content: Text( - "Artist URL copied to clipboard", + "Added ${topTracks.length} tracks to queue", textAlign: TextAlign.center, ), ), ); }, - ) - ], - ) - ], - ), - ), - ], - ), - const SizedBox(height: 50), - HookBuilder( - builder: (context) { - final topTracksQuery = useQueries.artist.topTracksOf( - ref, - artistId, - ); - - final isPlaylistPlaying = - playlistNotifier.isPlayingPlaylist( - topTracksQuery.data ?? [], - ); - - if (topTracksQuery.isLoading || !topTracksQuery.hasData) { - return const CircularProgressIndicator(); - } else if (topTracksQuery.hasError) { - return Center( - child: Text(topTracksQuery.error.toString()), - ); - } - - final topTracks = topTracksQuery.data!; - - void playPlaylist(List tracks, - {Track? currentTrack}) async { - currentTrack ??= tracks.first; - if (!isPlaylistPlaying) { - playlistNotifier.loadAndPlay(tracks, - active: tracks - .indexWhere((s) => s.id == currentTrack?.id)); - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != playlist?.activeTrack.id) { - await playlistNotifier.playTrack(currentTrack); - } - } - - return Column(children: [ - Row( - children: [ - Text( - "Top Tracks", - style: Theme.of(context).textTheme.headlineSmall, - ), - if (!isPlaylistPlaying) - IconButton( - icon: const Icon( - SpotubeIcons.queueAdd, ), - onPressed: () { - playlistNotifier.add(topTracks.toList()); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - "Added ${topTracks.length} tracks to queue", - textAlign: TextAlign.center, - ), - ), - ); - }, + const SizedBox(width: 5), + IconButton( + icon: Icon( + isPlaylistPlaying + ? SpotubeIcons.stop + : SpotubeIcons.play, + color: Colors.white, + ), + style: IconButton.styleFrom( + backgroundColor: + Theme.of(context).primaryColor, + ), + onPressed: () => + playPlaylist(topTracks.toList()), + ) + ], + ), + ...topTracks.toList().asMap().entries.map((track) { + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + return TrackTile( + playlist, + duration: duration, + track: track, + isActive: + playlist?.activeTrack.id == track.value.id, + onTrackPlayButtonPressed: (currentTrack) => + playPlaylist( + topTracks.toList(), + currentTrack: track.value, ), - const SizedBox(width: 5), - IconButton( - icon: Icon( - isPlaylistPlaying - ? SpotubeIcons.stop - : SpotubeIcons.play, - color: Colors.white, - ), - style: IconButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - ), - onPressed: () => playPlaylist(topTracks.toList()), - ) - ], - ), - ...topTracks.toList().asMap().entries.map((track) { - String duration = - "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return TrackTile( - playlist, - duration: duration, - track: track, - isActive: - playlist?.activeTrack.id == track.value.id, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - topTracks.toList(), - currentTrack: track.value, - ), - ); - }), - ]); - }, - ), - const SizedBox(height: 50), - Text( - "Albums", - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 10), - ArtistAlbumList(artistId), - const SizedBox(height: 20), - Text( - "Fans also likes", - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 10), - HookBuilder( - builder: (context) { - final relatedArtists = useQueries.artist.relatedArtistsOf( - ref, - artistId, - ); - - if (relatedArtists.isLoading || !relatedArtists.hasData) { - return const CircularProgressIndicator(); - } else if (relatedArtists.hasError) { - return Center( - child: Text(relatedArtists.error.toString()), + ); + }), + ]); + }, + ), + const SizedBox(height: 50), + Text( + "Albums", + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 10), + ArtistAlbumList(artistId), + const SizedBox(height: 20), + Text( + "Fans also likes", + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 10), + HookBuilder( + builder: (context) { + final relatedArtists = + useQueries.artist.relatedArtistsOf( + ref, + artistId, ); - } - return Center( - child: Wrap( - spacing: 20, - runSpacing: 20, - children: relatedArtists.data! - .map((artist) => ArtistCard(artist)) - .toList(), - ), - ); - }, - ), - ], + if (relatedArtists.isLoading || + !relatedArtists.hasData) { + return const CircularProgressIndicator(); + } else if (relatedArtists.hasError) { + return Center( + child: Text(relatedArtists.error.toString()), + ); + } + + return Center( + child: Wrap( + spacing: 20, + runSpacing: 20, + children: relatedArtists.data! + .map((artist) => ArtistCard(artist)) + .toList(), + ), + ); + }, + ), + ], + ), ), ); }, diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 3b997e10..7445b99e 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -77,7 +77,7 @@ class GenrePage extends HookConsumerWidget { if (searchText.value.isEmpty && index == categories.length - 1) { return const ShimmerCategories(); } - return CategoryCard(category); + return SafeArea(child: CategoryCard(category)); }, ), ), diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 3c80ba8c..9225db5b 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -75,21 +75,23 @@ class PersonalizedItemCard extends HookWidget { child: Waypoint( controller: scrollController, onTouchEdge: hasNextPage ? onFetchMore : null, - child: ListView( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), - children: [ - ...?playlistItems - ?.map((playlist) => PlaylistCard(playlist)), - ...?albumItems?.map( - (album) => AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album(album), + child: SafeArea( + child: ListView( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + controller: scrollController, + physics: const AlwaysScrollableScrollPhysics(), + children: [ + ...?playlistItems + ?.map((playlist) => PlaylistCard(playlist)), + ...?albumItems?.map( + (album) => AlbumCard( + TypeConversionUtils.simpleAlbum_X_Album(album), + ), ), - ), - if (hasNextPage) const ShimmerPlaybuttonCard(count: 1), - ], + if (hasNextPage) const ShimmerPlaybuttonCard(count: 1), + ], + ), ), ), ), diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index e809f039..4bc04b66 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -13,10 +13,10 @@ class LibraryPage extends HookConsumerWidget { const LibraryPage({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { - return const SafeArea( - bottom: false, - child: DefaultTabController( - length: 5, + return const DefaultTabController( + length: 5, + child: SafeArea( + bottom: false, child: Scaffold( appBar: PageWindowTitleBar( centerTitle: true, diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index fe339151..a131b1fc 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -65,7 +65,6 @@ class SearchPage extends HookConsumerWidget { bottom: false, child: Scaffold( appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar() : null, - extendBody: true, body: !authenticationNotifier.isLoggedIn ? const AnonymousFallback() : Column( @@ -128,241 +127,250 @@ class SearchPage extends HookConsumerWidget { vertical: 8, horizontal: 20, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (tracks.isNotEmpty) - Text( - "Songs", - style: - Theme.of(context).textTheme.titleLarge!, - ), - if (searchTrack.isLoadingPage) - const CircularProgressIndicator() - else if (searchTrack.hasPageError) - Text(searchTrack.errors.lastOrNull - ?.toString() ?? - "") - else - ...tracks.asMap().entries.map((track) { - String duration = - "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return TrackTile( - playlist, - track: track, - duration: duration, - isActive: playlist?.activeTrack.id == - track.value.id, - onTrackPlayButtonPressed: - (currentTrack) async { - final isTrackPlaying = - playlist?.activeTrack.id == - currentTrack.id; - if (!isTrackPlaying && - context.mounted) { - final shouldPlay = - (playlist?.tracks.length ?? 0) > - 20 - ? await showPromptDialog( - context: context, - title: - "Playing ${currentTrack.name}", - message: - "This will clear the current queue. " - "${playlist?.tracks.length ?? 0} tracks will be removed\n" - "Do you want to continue?", - ) - : true; + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (tracks.isNotEmpty) + Text( + "Songs", + style: Theme.of(context) + .textTheme + .titleLarge!, + ), + if (searchTrack.isLoadingPage) + const CircularProgressIndicator() + else if (searchTrack.hasPageError) + Text(searchTrack.errors.lastOrNull + ?.toString() ?? + "") + else + ...tracks.asMap().entries.map((track) { + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + return TrackTile( + playlist, + track: track, + duration: duration, + isActive: playlist?.activeTrack.id == + track.value.id, + onTrackPlayButtonPressed: + (currentTrack) async { + final isTrackPlaying = + playlist?.activeTrack.id == + currentTrack.id; + if (!isTrackPlaying && + context.mounted) { + final shouldPlay = + (playlist?.tracks.length ?? 0) > + 20 + ? await showPromptDialog( + context: context, + title: + "Playing ${currentTrack.name}", + message: + "This will clear the current queue. " + "${playlist?.tracks.length ?? 0} tracks will be removed\n" + "Do you want to continue?", + ) + : true; - if (shouldPlay) { - await playlistNotifier - .loadAndPlay([currentTrack]); + if (shouldPlay) { + await playlistNotifier + .loadAndPlay([currentTrack]); + } } - } - }, - ); - }), - if (searchTrack.hasNextPage && - tracks.isNotEmpty) - Center( - child: TextButton( - onPressed: searchTrack.isRefreshingPage - ? null - : () => searchTrack.fetchNext(), - child: searchTrack.isRefreshingPage - ? const CircularProgressIndicator() - : const Text("Load more"), + }, + ); + }), + if (searchTrack.hasNextPage && + tracks.isNotEmpty) + Center( + child: TextButton( + onPressed: searchTrack.isRefreshingPage + ? null + : () => searchTrack.fetchNext(), + child: searchTrack.isRefreshingPage + ? const CircularProgressIndicator() + : const Text("Load more"), + ), ), - ), - if (playlists.isNotEmpty) - Text( - "Playlists", - style: - Theme.of(context).textTheme.titleLarge!, - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - scrollbarOrientation: - breakpoint > Breakpoints.md - ? ScrollbarOrientation.bottom - : ScrollbarOrientation.top, - controller: playlistController, - child: Waypoint( - onTouchEdge: () { - searchPlaylist.fetchNext(); + if (playlists.isNotEmpty) + Text( + "Playlists", + style: Theme.of(context) + .textTheme + .titleLarge!, + ), + const SizedBox(height: 10), + ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, }, + ), + child: Scrollbar( + scrollbarOrientation: + breakpoint > Breakpoints.md + ? ScrollbarOrientation.bottom + : ScrollbarOrientation.top, controller: playlistController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, + child: Waypoint( + onTouchEdge: () { + searchPlaylist.fetchNext(); + }, controller: playlistController, - child: Row( - children: [ - ...playlists.mapIndexed( - (i, playlist) { - if (i == playlists.length - 1 && - searchPlaylist - .hasNextPage) { - return const ShimmerPlaybuttonCard( - count: 1); - } - return PlaylistCard(playlist); - }, - ), - ], + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: playlistController, + child: Row( + children: [ + ...playlists.mapIndexed( + (i, playlist) { + if (i == + playlists.length - + 1 && + searchPlaylist + .hasNextPage) { + return const ShimmerPlaybuttonCard( + count: 1); + } + return PlaylistCard(playlist); + }, + ), + ], + ), ), ), ), ), - ), - if (searchPlaylist.isLoadingPage) - const CircularProgressIndicator(), - if (searchPlaylist.hasPageError) - Text( - searchPlaylist.errors.lastOrNull - ?.toString() ?? - "", - ), - const SizedBox(height: 20), - if (artists.isNotEmpty) - Text( - "Artists", - style: - Theme.of(context).textTheme.titleLarge!, - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: artistController, - child: Waypoint( + if (searchPlaylist.isLoadingPage) + const CircularProgressIndicator(), + if (searchPlaylist.hasPageError) + Text( + searchPlaylist.errors.lastOrNull + ?.toString() ?? + "", + ), + const SizedBox(height: 20), + if (artists.isNotEmpty) + Text( + "Artists", + style: Theme.of(context) + .textTheme + .titleLarge!, + ), + const SizedBox(height: 10), + ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( controller: artistController, - onTouchEdge: () { - searchArtist.fetchNext(); - }, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, + child: Waypoint( controller: artistController, - child: Row( - children: [ - ...artists.mapIndexed( - (i, artist) { - if (i == artists.length - 1 && - searchArtist.hasNextPage) { + onTouchEdge: () { + searchArtist.fetchNext(); + }, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: artistController, + child: Row( + children: [ + ...artists.mapIndexed( + (i, artist) { + if (i == artists.length - 1 && + searchArtist + .hasNextPage) { + return const ShimmerPlaybuttonCard( + count: 1); + } + return Container( + margin: const EdgeInsets + .symmetric( + horizontal: 15), + child: ArtistCard(artist), + ); + }, + ), + ], + ), + ), + ), + ), + ), + if (searchArtist.isLoadingPage) + const CircularProgressIndicator(), + if (searchArtist.hasPageError) + Text( + searchArtist.errors.lastOrNull + ?.toString() ?? + "", + ), + const SizedBox(height: 20), + if (albums.isNotEmpty) + Text( + "Albums", + style: Theme.of(context) + .textTheme + .titleMedium!, + ), + const SizedBox(height: 10), + ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: albumController, + child: Waypoint( + controller: albumController, + onTouchEdge: () { + searchAlbum.fetchNext(); + }, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: albumController, + child: Row( + children: [ + ...albums.mapIndexed((i, album) { + if (i == albums.length - 1 && + searchAlbum.hasNextPage) { return const ShimmerPlaybuttonCard( count: 1); } - return Container( - margin: const EdgeInsets - .symmetric( - horizontal: 15), - child: ArtistCard(artist), + return AlbumCard( + TypeConversionUtils + .simpleAlbum_X_Album( + album, + ), ); - }, - ), - ], + }), + ], + ), ), ), ), ), - ), - if (searchArtist.isLoadingPage) - const CircularProgressIndicator(), - if (searchArtist.hasPageError) - Text( - searchArtist.errors.lastOrNull - ?.toString() ?? - "", - ), - const SizedBox(height: 20), - if (albums.isNotEmpty) - Text( - "Albums", - style: Theme.of(context) - .textTheme - .titleMedium!, - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: albumController, - child: Waypoint( - controller: albumController, - onTouchEdge: () { - searchAlbum.fetchNext(); - }, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: albumController, - child: Row( - children: [ - ...albums.mapIndexed((i, album) { - if (i == albums.length - 1 && - searchAlbum.hasNextPage) { - return const ShimmerPlaybuttonCard( - count: 1); - } - return AlbumCard( - TypeConversionUtils - .simpleAlbum_X_Album( - album, - ), - ); - }), - ], - ), - ), + if (searchAlbum.isLoadingPage) + const CircularProgressIndicator(), + if (searchAlbum.hasPageError) + Text( + searchAlbum.errors.lastOrNull + ?.toString() ?? + "", ), - ), - ), - if (searchAlbum.isLoadingPage) - const CircularProgressIndicator(), - if (searchAlbum.hasPageError) - Text( - searchAlbum.errors.lastOrNull?.toString() ?? - "", - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 6e51a4cd..27dec2e1 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -17,9 +17,9 @@ class AboutSpotube extends HookConsumerWidget { final packageInfo = usePackageInfo(); return Scaffold( - appBar: PageWindowTitleBar( - leading: const BackButton(), - title: const Text("About Spotube"), + appBar: const PageWindowTitleBar( + leading: BackButton(), + title: Text("About Spotube"), ), body: SingleChildScrollView( child: Padding(