chore: fix shuffle doesn't move active track to top and library gridview with floating filter field

This commit is contained in:
Kingkor Roy Tirtho 2024-04-15 19:11:59 +06:00
parent 6e07fec1a5
commit 6f4c308457
9 changed files with 212 additions and 207 deletions

View File

@ -52,7 +52,11 @@ class ConnectDeviceButton extends HookConsumerWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
fit: StackFit.loose, fit: StackFit.loose,
children: [ children: [
Center( Material(
type: MaterialType.transparency,
child: Center(
child: ClipRect(
clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
ServiceUtils.push(context, "/connect"); ServiceUtils.push(context, "/connect");
@ -86,8 +90,8 @@ class ConnectDeviceButton extends HookConsumerWidget {
Text( Text(
" (${connectClients.asData?.value.services.length})", " (${connectClients.asData?.value.services.length})",
style: TextStyle( style: TextStyle(
color: color: colorScheme.onPrimaryContainer
colorScheme.onPrimaryContainer.withOpacity(0.5), .withOpacity(0.5),
), ),
), ),
const Gap(35), const Gap(35),
@ -96,8 +100,10 @@ class ConnectDeviceButton extends HookConsumerWidget {
), ),
), ),
), ),
),
),
Positioned( Positioned(
right: 0, right: -3,
child: IconButton.filled( child: IconButton.filled(
icon: const Icon(SpotubeIcons.speaker), icon: const Icon(SpotubeIcons.speaker),
style: IconButton.styleFrom( style: IconButton.styleFrom(

View File

@ -2,17 +2,17 @@ import 'package:flutter/material.dart' hide Image;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.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/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.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/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
@ -50,20 +50,21 @@ class UserAlbums extends HookConsumerWidget {
return const AnonymousFallback(); return const AnonymousFallback();
} }
final theme = Theme.of(context); return SafeArea(
child: Scaffold(
return RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
ref.invalidate(favoriteAlbumsProvider); ref.invalidate(favoriteAlbumsProvider);
}, },
child: SafeArea( child: InterScrollbar(
child: Scaffold( controller: controller,
appBar: PreferredSize( child: CustomScrollView(
preferredSize: const Size.fromHeight(50), controller: controller,
child: Padding( slivers: [
SliverAppBar(
floating: true,
flexibleSpace: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ColoredBox(
color: theme.scaffoldBackgroundColor,
child: SearchBar( child: SearchBar(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
leading: const Icon(SpotubeIcons.filter), leading: const Icon(SpotubeIcons.filter),
@ -71,54 +72,47 @@ class UserAlbums extends HookConsumerWidget {
), ),
), ),
), ),
), const SliverGap(10),
body: SizedBox.expand( Skeletonizer.sliver(
child: InterScrollbar(
controller: controller,
child: SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
controller: controller,
child: Skeletonizer(
enabled: albumsQuery.isLoading, enabled: albumsQuery.isLoading,
child: Center( child: SliverLayoutBuilder(builder: (context, constrains) {
child: Wrap( return SliverGrid.builder(
runSpacing: 20, itemCount: albums.isEmpty ? 6 : albums.length + 1,
alignment: WrapAlignment.center, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
runAlignment: WrapAlignment.center, maxCrossAxisExtent: 200,
crossAxisAlignment: WrapCrossAlignment.center, mainAxisExtent: constrains.smAndDown ? 225 : 250,
children: [ crossAxisSpacing: 8,
if (albumsQuery.asData?.value == null || mainAxisSpacing: 8,
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()), itemBuilder: (context, index) {
if (albums.isNotEmpty && if (albums.isNotEmpty && index == albums.length) {
albumsQuery.asData?.value.hasMore == true) if (albumsQuery.asData?.value.hasMore != true) {
Skeletonizer( return const SizedBox.shrink();
enabled: true, }
child: Waypoint(
return Waypoint(
controller: controller, controller: controller,
isGrid: true, isGrid: true,
onTouchEdge: albumsQueryNotifier.fetchMore, onTouchEdge: albumsQueryNotifier.fetchMore,
child: AlbumCard(FakeData.album), child: Skeletonizer(
enabled: true,
child: AlbumCard(FakeData.albumSimple),
),
);
}
return AlbumCard(
albums.elementAtOrNull(index) ?? FakeData.albumSimple,
);
},
);
}),
), ),
)
], ],
), ),
), ),
), ),
), ),
),
),
),
),
); );
} }
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.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/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/artist/artist_card.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/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/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
@ -20,10 +22,10 @@ class UserArtists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final artistQuery = ref.watch(followedArtistsProvider); final artistQuery = ref.watch(followedArtistsProvider);
final artistQueryNotifier = ref.watch(followedArtistsProvider.notifier);
final searchText = useState(''); final searchText = useState('');
@ -50,72 +52,68 @@ class UserArtists extends HookConsumerWidget {
return const AnonymousFallback(); return const AnonymousFallback();
} }
return Scaffold( return SafeArea(
appBar: PreferredSize( child: Scaffold(
preferredSize: const Size.fromHeight(50), body: RefreshIndicator(
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,
),
),
),
),
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 { onRefresh: () async {
ref.invalidate(followedArtistsProvider); ref.invalidate(followedArtistsProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
child: SingleChildScrollView( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CustomScrollView(
controller: controller, controller: controller,
child: SizedBox( slivers: [
width: double.infinity, SliverAppBar(
child: SafeArea( floating: true,
child: Center( flexibleSpace: SearchBar(
child: Skeletonizer( onChanged: (value) => searchText.value = value,
leading: const Icon(SpotubeIcons.filter),
hintText: context.l10n.filter_artist,
),
),
const SliverGap(10),
Skeletonizer.sliver(
enabled: artistQuery.isLoading, enabled: artistQuery.isLoading,
child: Wrap( child: SliverLayoutBuilder(builder: (context, constrains) {
spacing: 15, return SliverGrid.builder(
runSpacing: 5, itemCount: filteredArtists.isEmpty
children: artistQuery.isLoading ? 6
? List.generate( : filteredArtists.length + 1,
10, (index) => ArtistCard(FakeData.artist)) gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
: filteredArtists.isEmpty maxCrossAxisExtent: 200,
? [ mainAxisExtent: constrains.smAndDown ? 225 : 250,
const Row( crossAxisSpacing: 8,
mainAxisAlignment: mainAxisSpacing: 8,
MainAxisAlignment.center, ),
children: [ itemBuilder: (context, index) {
NotFound(), 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,
);
},
);
}),
),
], ],
)
]
: filteredArtists
.mapIndexed(
(index, artist) => ArtistCard(artist),
)
.toList(),
),
),
),
), ),
), ),
), ),

View File

@ -176,7 +176,7 @@ class UserLocalTracks extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 10), const SizedBox(width: 5),
FilledButton( FilledButton(
onPressed: trackSnapshot.asData?.value != null onPressed: trackSnapshot.asData?.value != null
? () async { ? () async {
@ -212,7 +212,7 @@ class UserLocalTracks extends HookConsumerWidget {
sortBy.value = value; sortBy.value = value;
}, },
), ),
const SizedBox(width: 10), const SizedBox(width: 5),
FilledButton( FilledButton(
child: const Icon(SpotubeIcons.refresh), child: const Icon(SpotubeIcons.refresh),
onPressed: () { onPressed: () {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart' hide Image;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.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/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart';
class UserPlaylists extends HookConsumerWidget { class UserPlaylists extends HookConsumerWidget {
const UserPlaylists({super.key}); const UserPlaylists({super.key});
@ -86,23 +88,24 @@ class UserPlaylists extends HookConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
controller: controller, controller: controller,
slivers: [ slivers: [
SliverToBoxAdapter( SliverAppBar(
child: Column( floating: true,
mainAxisSize: MainAxisSize.min, flexibleSpace: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 8),
Padding(
padding: const EdgeInsets.all(10),
child: SearchBar( child: SearchBar(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
hintText: context.l10n.filter_playlists, hintText: context.l10n.filter_playlists,
leading: const Icon(SpotubeIcons.filter), leading: const Icon(SpotubeIcons.filter),
), ),
), ),
Row( bottom: PreferredSize(
preferredSize:
Size.fromHeight(kIsDesktop ? 35 : kToolbarHeight),
child: Row(
children: [ children: [
const SizedBox(width: 10), const Gap(10),
const PlaylistCreateDialogButton(), const PlaylistCreateDialogButton(),
const SizedBox(width: 10), const Gap(10),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(SpotubeIcons.magic), icon: const Icon(SpotubeIcons.magic),
label: Text(context.l10n.generate_playlist), label: Text(context.l10n.generate_playlist),
@ -110,15 +113,12 @@ class UserPlaylists extends HookConsumerWidget {
GoRouter.of(context).push("/library/generate"); 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) { SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid.builder( return SliverGrid.builder(
itemCount: playlists.isEmpty ? 6 : playlists.length + 1, itemCount: playlists.isEmpty ? 6 : playlists.length + 1,

View File

@ -23,6 +23,7 @@ class ConnectPage extends HookConsumerWidget {
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
title: Text(context.l10n.devices), title: Text(context.l10n.devices),
titleSpacing: 0,
), ),
body: ListTileTheme( body: ListTileTheme(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

View File

@ -26,6 +26,7 @@ class GenrePage extends HookConsumerWidget {
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
title: Text(context.l10n.explore_genres), title: Text(context.l10n.explore_genres),
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
titleSpacing: 0,
), ),
body: SafeArea( body: SafeArea(
top: false, top: false,

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/connect/connect_device.dart';
import 'package:spotube/components/home/sections/featured.dart'; import 'package:spotube/components/home/sections/featured.dart';
import 'package:spotube/components/home/sections/feed.dart'; import 'package:spotube/components/home/sections/feed.dart';
@ -34,8 +34,9 @@ class HomePage extends HookConsumerWidget {
controller: controller, controller: controller,
slivers: [ slivers: [
if (mediaQuery.mdAndDown) if (mediaQuery.mdAndDown)
PageWindowTitleBar.sliver( SliverAppBar(
pinned: DesktopTools.platform.isDesktop, floating: true,
title: Assets.spotubeLogoPng.image(height: 45),
actions: [ actions: [
const ConnectDeviceButton(), const ConnectDeviceButton(),
const Gap(10), const Gap(10),

View File

@ -106,6 +106,10 @@ class CustomPlayer extends Player {
_shuffled = shuffle; _shuffled = shuffle;
await super.setShuffle(shuffle); await super.setShuffle(shuffle);
_shuffleStream.add(shuffle); _shuffleStream.add(shuffle);
await Future.delayed(const Duration(milliseconds: 100));
if (shuffle) {
await move(state.playlist.index, 0);
}
} }
@override @override