diff --git a/lib/components/connect/connect_device.dart b/lib/components/connect/connect_device.dart index 14243fa8..3ac585df 100644 --- a/lib/components/connect/connect_device.dart +++ b/lib/components/connect/connect_device.dart @@ -52,52 +52,58 @@ class ConnectDeviceButton extends HookConsumerWidget { alignment: Alignment.centerRight, fit: StackFit.loose, children: [ - Center( - child: InkWell( - onTap: () { - ServiceUtils.push(context, "/connect"); - }, - borderRadius: BorderRadius.circular(50), - child: Ink( - decoration: BoxDecoration( + Material( + type: MaterialType.transparency, + child: Center( + child: ClipRect( + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () { + ServiceUtils.push(context, "/connect"); + }, borderRadius: BorderRadius.circular(50), - color: colorScheme.primaryContainer, - ), - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (connectClients.asData?.value.resolvedService != - null) ...[ - Container( - width: 7, - height: 7, - decoration: BoxDecoration( - color: Colors.greenAccent, - borderRadius: BorderRadius.circular(50), - ), - ), - const Gap(5), - ], - Text(context.l10n.devices), - if (connectClients.asData?.value.services.isNotEmpty == - true) - Text( - " (${connectClients.asData?.value.services.length})", - style: TextStyle( - color: - colorScheme.onPrimaryContainer.withOpacity(0.5), - ), - ), - const Gap(35), - ], + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: colorScheme.primaryContainer, + ), + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (connectClients.asData?.value.resolvedService != + null) ...[ + Container( + width: 7, + height: 7, + decoration: BoxDecoration( + color: Colors.greenAccent, + borderRadius: BorderRadius.circular(50), + ), + ), + const Gap(5), + ], + Text(context.l10n.devices), + if (connectClients.asData?.value.services.isNotEmpty == + true) + Text( + " (${connectClients.asData?.value.services.length})", + style: TextStyle( + color: colorScheme.onPrimaryContainer + .withOpacity(0.5), + ), + ), + const Gap(35), + ], + ), + ), ), ), ), ), Positioned( - right: 0, + right: -3, child: IconButton.filled( icon: const Icon(SpotubeIcons.speaker), style: IconButton.styleFrom( diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 43fa0165..e1b82113 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -2,17 +2,17 @@ import 'package:flutter/material.dart' hide Image; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/album/album_card.dart'; -import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/waypoint.dart'; -import 'package:spotube/extensions/album_simple.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -50,71 +50,65 @@ class UserAlbums extends HookConsumerWidget { return const AnonymousFallback(); } - final theme = Theme.of(context); - - return RefreshIndicator( - onRefresh: () async { - ref.invalidate(favoriteAlbumsProvider); - }, - child: SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(50), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ColoredBox( - color: theme.scaffoldBackgroundColor, - child: SearchBar( - onChanged: (value) => searchText.value = value, - leading: const Icon(SpotubeIcons.filter), - hintText: context.l10n.filter_albums, - ), - ), - ), - ), - body: SizedBox.expand( - child: InterScrollbar( + return SafeArea( + child: Scaffold( + body: RefreshIndicator( + onRefresh: () async { + ref.invalidate(favoriteAlbumsProvider); + }, + child: InterScrollbar( + controller: controller, + child: CustomScrollView( controller: controller, - child: SingleChildScrollView( - padding: const EdgeInsets.all(8.0), - controller: controller, - child: Skeletonizer( - enabled: albumsQuery.isLoading, - child: Center( - child: Wrap( - runSpacing: 20, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (albumsQuery.asData?.value == null || - albumsQuery.asData!.value.items.isEmpty) - ...List.generate( - 10, - (index) => AlbumCard(FakeData.album), - ) - else if (albums.isEmpty) - const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [NotFound()], - ), - for (final album in albums) AlbumCard(album.toAlbum()), - if (albums.isNotEmpty && - albumsQuery.asData?.value.hasMore == true) - Skeletonizer( - enabled: true, - child: Waypoint( - controller: controller, - isGrid: true, - onTouchEdge: albumsQueryNotifier.fetchMore, - child: AlbumCard(FakeData.album), - ), - ) - ], + slivers: [ + SliverAppBar( + floating: true, + flexibleSpace: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SearchBar( + onChanged: (value) => searchText.value = value, + leading: const Icon(SpotubeIcons.filter), + hintText: context.l10n.filter_albums, ), ), ), - ), + const SliverGap(10), + Skeletonizer.sliver( + enabled: albumsQuery.isLoading, + child: SliverLayoutBuilder(builder: (context, constrains) { + return SliverGrid.builder( + itemCount: albums.isEmpty ? 6 : albums.length + 1, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: constrains.smAndDown ? 225 : 250, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context, index) { + if (albums.isNotEmpty && index == albums.length) { + if (albumsQuery.asData?.value.hasMore != true) { + return const SizedBox.shrink(); + } + + return Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: albumsQueryNotifier.fetchMore, + child: Skeletonizer( + enabled: true, + child: AlbumCard(FakeData.albumSimple), + ), + ); + } + + return AlbumCard( + albums.elementAtOrNull(index) ?? FakeData.albumSimple, + ); + }, + ); + }), + ), + ], ), ), ), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index 83db35c6..0ef0ff39 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/fake.dart'; @@ -9,8 +10,9 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/artist/artist_card.dart'; -import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; +import 'package:spotube/components/shared/waypoint.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -20,10 +22,10 @@ class UserArtists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final theme = Theme.of(context); final auth = ref.watch(authenticationProvider); final artistQuery = ref.watch(followedArtistsProvider); + final artistQueryNotifier = ref.watch(followedArtistsProvider.notifier); final searchText = useState(''); @@ -50,77 +52,73 @@ class UserArtists extends HookConsumerWidget { return const AnonymousFallback(); } - return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(50), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: ColoredBox( - color: theme.scaffoldBackgroundColor, - child: SearchBar( - onChanged: (value) => searchText.value = value, - leading: const Icon(SpotubeIcons.filter), - hintText: context.l10n.filter_artist, + return SafeArea( + child: Scaffold( + body: RefreshIndicator( + onRefresh: () async { + ref.invalidate(followedArtistsProvider); + }, + child: InterScrollbar( + controller: controller, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: CustomScrollView( + controller: controller, + slivers: [ + SliverAppBar( + floating: true, + flexibleSpace: SearchBar( + onChanged: (value) => searchText.value = value, + leading: const Icon(SpotubeIcons.filter), + hintText: context.l10n.filter_artist, + ), + ), + const SliverGap(10), + Skeletonizer.sliver( + enabled: artistQuery.isLoading, + child: SliverLayoutBuilder(builder: (context, constrains) { + return SliverGrid.builder( + itemCount: filteredArtists.isEmpty + ? 6 + : filteredArtists.length + 1, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: constrains.smAndDown ? 225 : 250, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context, index) { + if (filteredArtists.isNotEmpty && + index == filteredArtists.length) { + if (artistQuery.asData?.value.hasMore != true) { + return const SizedBox.shrink(); + } + + return Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: artistQueryNotifier.fetchMore, + child: Skeletonizer( + enabled: true, + child: ArtistCard(FakeData.artist), + ), + ); + } + + return ArtistCard( + filteredArtists.elementAtOrNull(index) ?? + FakeData.artist, + ); + }, + ); + }), + ), + ], + ), ), ), ), ), - backgroundColor: theme.scaffoldBackgroundColor, - body: artistQuery.asData?.value.items.isEmpty == true - ? Padding( - padding: const EdgeInsets.all(20), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(width: 10), - Text(context.l10n.loading), - ], - ), - ) - : RefreshIndicator( - onRefresh: () async { - ref.invalidate(followedArtistsProvider); - }, - child: InterScrollbar( - controller: controller, - child: SingleChildScrollView( - controller: controller, - child: SizedBox( - width: double.infinity, - child: SafeArea( - child: Center( - child: Skeletonizer( - enabled: artistQuery.isLoading, - child: Wrap( - spacing: 15, - runSpacing: 5, - children: artistQuery.isLoading - ? List.generate( - 10, (index) => ArtistCard(FakeData.artist)) - : filteredArtists.isEmpty - ? [ - const Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - NotFound(), - ], - ) - ] - : filteredArtists - .mapIndexed( - (index, artist) => ArtistCard(artist), - ) - .toList(), - ), - ), - ), - ), - ), - ), - ), - ), ); } } diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index f8bd1326..a7b2102b 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -176,7 +176,7 @@ class UserLocalTracks extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Row( children: [ - const SizedBox(width: 10), + const SizedBox(width: 5), FilledButton( onPressed: trackSnapshot.asData?.value != null ? () async { @@ -212,7 +212,7 @@ class UserLocalTracks extends HookConsumerWidget { sortBy.value = value; }, ), - const SizedBox(width: 10), + const SizedBox(width: 5), FilledButton( child: const Icon(SpotubeIcons.refresh), onPressed: () { diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 563541de..069dfad9 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart' hide Image; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:collection/collection.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -18,6 +19,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart'; +import 'package:spotube/utils/platform.dart'; class UserPlaylists extends HookConsumerWidget { const UserPlaylists({super.key}); @@ -86,39 +88,37 @@ class UserPlaylists extends HookConsumerWidget { child: CustomScrollView( controller: controller, slivers: [ - SliverToBoxAdapter( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: SearchBar( - onChanged: (value) => searchText.value = value, - hintText: context.l10n.filter_playlists, - leading: const Icon(SpotubeIcons.filter), + SliverAppBar( + floating: true, + flexibleSpace: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: SearchBar( + onChanged: (value) => searchText.value = value, + hintText: context.l10n.filter_playlists, + leading: const Icon(SpotubeIcons.filter), + ), + ), + bottom: PreferredSize( + preferredSize: + Size.fromHeight(kIsDesktop ? 35 : kToolbarHeight), + child: Row( + children: [ + const Gap(10), + const PlaylistCreateDialogButton(), + const Gap(10), + ElevatedButton.icon( + icon: const Icon(SpotubeIcons.magic), + label: Text(context.l10n.generate_playlist), + onPressed: () { + GoRouter.of(context).push("/library/generate"); + }, ), - ), - Row( - children: [ - const SizedBox(width: 10), - const PlaylistCreateDialogButton(), - const SizedBox(width: 10), - ElevatedButton.icon( - icon: const Icon(SpotubeIcons.magic), - label: Text(context.l10n.generate_playlist), - onPressed: () { - GoRouter.of(context).push("/library/generate"); - }, - ), - const SizedBox(width: 10), - ], - ), - ], + const Gap(10), + ], + ), ), ), - const SliverToBoxAdapter( - child: SizedBox(height: 10), - ), + const SliverGap(10), SliverLayoutBuilder(builder: (context, constrains) { return SliverGrid.builder( itemCount: playlists.isEmpty ? 6 : playlists.length + 1, diff --git a/lib/pages/connect/connect.dart b/lib/pages/connect/connect.dart index 170a0c72..cbdb446e 100644 --- a/lib/pages/connect/connect.dart +++ b/lib/pages/connect/connect.dart @@ -23,6 +23,7 @@ class ConnectPage extends HookConsumerWidget { appBar: PageWindowTitleBar( automaticallyImplyLeading: true, title: Text(context.l10n.devices), + titleSpacing: 0, ), body: ListTileTheme( shape: RoundedRectangleBorder( diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart index a981cbe7..291ce737 100644 --- a/lib/pages/home/genres/genres.dart +++ b/lib/pages/home/genres/genres.dart @@ -26,6 +26,7 @@ class GenrePage extends HookConsumerWidget { appBar: PageWindowTitleBar( title: Text(context.l10n.explore_genres), automaticallyImplyLeading: true, + titleSpacing: 0, ), body: SafeArea( top: false, diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index d5639274..31f26bee 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/connect/connect_device.dart'; import 'package:spotube/components/home/sections/featured.dart'; import 'package:spotube/components/home/sections/feed.dart'; @@ -34,8 +34,9 @@ class HomePage extends HookConsumerWidget { controller: controller, slivers: [ if (mediaQuery.mdAndDown) - PageWindowTitleBar.sliver( - pinned: DesktopTools.platform.isDesktop, + SliverAppBar( + floating: true, + title: Assets.spotubeLogoPng.image(height: 45), actions: [ const ConnectDeviceButton(), const Gap(10), diff --git a/lib/services/audio_player/custom_player.dart b/lib/services/audio_player/custom_player.dart index d273519e..916a983f 100644 --- a/lib/services/audio_player/custom_player.dart +++ b/lib/services/audio_player/custom_player.dart @@ -106,6 +106,10 @@ class CustomPlayer extends Player { _shuffled = shuffle; await super.setShuffle(shuffle); _shuffleStream.add(shuffle); + await Future.delayed(const Duration(milliseconds: 100)); + if (shuffle) { + await move(state.playlist.index, 0); + } } @override