mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: grid/list customizable playbutton view
This commit is contained in:
parent
05d544fe5a
commit
a6720d5392
@ -130,4 +130,6 @@ abstract class SpotubeIcons {
|
||||
static const open = FeatherIcons.externalLink;
|
||||
static const radioChecked = Icons.radio_button_on_rounded;
|
||||
static const radioUnchecked = Icons.radio_button_off_rounded;
|
||||
static const grid = FeatherIcons.grid;
|
||||
static const list = FeatherIcons.list;
|
||||
}
|
||||
|
@ -139,7 +139,9 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||
|
||||
if (mediaQuery.mdAndUp) {
|
||||
return Tooltip(
|
||||
tooltip: Text(tooltip ?? ''),
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(tooltip ?? ''),
|
||||
),
|
||||
child: IconButton.ghost(
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||
onPressed: () {
|
||||
@ -162,7 +164,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||
|
||||
if (child != null) {
|
||||
return Tooltip(
|
||||
tooltip: Text(tooltip ?? ''),
|
||||
tooltip: TooltipContainer(child: Text(tooltip ?? '')),
|
||||
child: Button(
|
||||
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||
style: const ButtonStyle.ghost(),
|
||||
@ -172,7 +174,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
tooltip: Text(tooltip ?? ''),
|
||||
tooltip: TooltipContainer(child: Text(tooltip ?? '')),
|
||||
child: IconButton.ghost(
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||
|
@ -2,13 +2,19 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
|
||||
class BackButton extends StatelessWidget {
|
||||
const BackButton({super.key});
|
||||
final Color? color;
|
||||
const BackButton({
|
||||
super.key,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton.ghost(
|
||||
size: const ButtonSize(.9),
|
||||
icon: const Icon(SpotubeIcons.angleLeft),
|
||||
icon: color != null
|
||||
? Icon(SpotubeIcons.angleLeft, color: color)
|
||||
: const Icon(SpotubeIcons.angleLeft),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/modules/album/album_card.dart';
|
||||
import 'package:spotube/modules/artist/artist_card.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||
@ -38,12 +37,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = useScrollController();
|
||||
final height = useBreakpointValue<double>(
|
||||
xs: 226,
|
||||
sm: 226,
|
||||
md: 236,
|
||||
others: 266,
|
||||
);
|
||||
final isArtist = items.every((s) => s is Artist);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@ -64,7 +58,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: height,
|
||||
height: isArtist ? 250 : 225,
|
||||
child: NotificationListener(
|
||||
// disable multiple scrollbar to use this
|
||||
onNotification: (notification) => true,
|
||||
@ -88,7 +82,9 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||
onFetchData: onFetchMore,
|
||||
loadingBuilder: (context) => Skeletonizer(
|
||||
enabled: true,
|
||||
child: AlbumCard(FakeData.albumSimple),
|
||||
child: isArtist
|
||||
? ArtistCard(FakeData.artist)
|
||||
: AlbumCard(FakeData.albumSimple),
|
||||
),
|
||||
isLoading: isLoadingNextPage,
|
||||
hasReachedMax: !hasNextPage,
|
||||
@ -100,11 +96,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||
PlaylistSimple() =>
|
||||
PlaylistCard(item as PlaylistSimple),
|
||||
AlbumSimple() => AlbumCard(item as AlbumSimple),
|
||||
Artist() => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0),
|
||||
child: ArtistCard(item as Artist),
|
||||
),
|
||||
Artist() => ArtistCard(item as Artist),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}),
|
||||
|
@ -1,17 +1,15 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/string.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class PlaybuttonCard extends HookWidget {
|
||||
class PlaybuttonCard extends StatelessWidget {
|
||||
final void Function()? onTap;
|
||||
final void Function()? onPlaybuttonPressed;
|
||||
final void Function()? onAddToQueuePressed;
|
||||
final String? description;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
final String imageUrl;
|
||||
final bool isPlaying;
|
||||
final bool isLoading;
|
||||
@ -23,7 +21,6 @@ class PlaybuttonCard extends HookWidget {
|
||||
required this.isPlaying,
|
||||
required this.isLoading,
|
||||
required this.title,
|
||||
this.margin,
|
||||
this.description,
|
||||
this.onPlaybuttonPressed,
|
||||
this.onAddToQueuePressed,
|
||||
@ -56,13 +53,16 @@ class PlaybuttonCard extends HookWidget {
|
||||
AnimatedScale(
|
||||
curve: Curves.easeOutBack,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
scale: states.contains(WidgetState.hovered) || kIsMobile
|
||||
scale: (states.contains(WidgetState.hovered) ||
|
||||
kIsMobile) &&
|
||||
!isLoading
|
||||
? 1
|
||||
: 0.7,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity:
|
||||
states.contains(WidgetState.hovered) || kIsMobile
|
||||
opacity: (states.contains(WidgetState.hovered) ||
|
||||
kIsMobile) &&
|
||||
!isLoading
|
||||
? 1
|
||||
: 0,
|
||||
child: IconButton.secondary(
|
||||
@ -76,17 +76,29 @@ class PlaybuttonCard extends HookWidget {
|
||||
AnimatedScale(
|
||||
curve: Curves.easeOutBack,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
scale: states.contains(WidgetState.hovered) || kIsMobile
|
||||
scale: states.contains(WidgetState.hovered) ||
|
||||
kIsMobile ||
|
||||
isPlaying ||
|
||||
isLoading
|
||||
? 1
|
||||
: 0.7,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
opacity:
|
||||
states.contains(WidgetState.hovered) || kIsMobile
|
||||
opacity: states.contains(WidgetState.hovered) ||
|
||||
kIsMobile ||
|
||||
isPlaying ||
|
||||
isLoading
|
||||
? 1
|
||||
: 0,
|
||||
child: IconButton.secondary(
|
||||
icon: const Icon(SpotubeIcons.play),
|
||||
icon: switch ((isLoading, isPlaying)) {
|
||||
(true, _) => const CircularProgressIndicator(
|
||||
size: 15,
|
||||
),
|
||||
(false, false) => const Icon(SpotubeIcons.play),
|
||||
(false, true) => const Icon(SpotubeIcons.pause)
|
||||
},
|
||||
enabled: !isLoading,
|
||||
onPressed: onPlaybuttonPressed,
|
||||
size: ButtonSize.small,
|
||||
),
|
||||
@ -96,11 +108,23 @@ class PlaybuttonCard extends HookWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
if (isOwner)
|
||||
const Positioned(
|
||||
right: 5,
|
||||
top: 5,
|
||||
child: SecondaryBadge(
|
||||
style: ButtonStyle.secondaryIcon(
|
||||
shape: ButtonShape.circle,
|
||||
size: ButtonSize.small,
|
||||
),
|
||||
child: Icon(SpotubeIcons.user),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Tooltip(
|
||||
tooltip: Text(title),
|
||||
tooltip: TooltipContainer(child: Text(title)),
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
92
lib/components/playbutton_view/playbutton_tile.dart
Normal file
92
lib/components/playbutton_view/playbutton_tile.dart
Normal file
@ -0,0 +1,92 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/string.dart';
|
||||
|
||||
class PlaybuttonTile extends StatelessWidget {
|
||||
final void Function()? onTap;
|
||||
final void Function()? onPlaybuttonPressed;
|
||||
final void Function()? onAddToQueuePressed;
|
||||
final String? description;
|
||||
|
||||
final String imageUrl;
|
||||
final bool isPlaying;
|
||||
final bool isLoading;
|
||||
final String title;
|
||||
final bool isOwner;
|
||||
|
||||
const PlaybuttonTile({
|
||||
required this.imageUrl,
|
||||
required this.isPlaying,
|
||||
required this.isLoading,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.onPlaybuttonPressed,
|
||||
this.onAddToQueuePressed,
|
||||
this.onTap,
|
||||
this.isOwner = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cleanDescription = description?.unescapeHtml().cleanHtml() ?? "";
|
||||
|
||||
return Button.ghost(
|
||||
leading: ClipRRect(
|
||||
borderRadius: context.theme.borderRadiusMd,
|
||||
child: UniversalImage(
|
||||
path: imageUrl,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(child: Text(context.l10n.add_to_queue)),
|
||||
child: IconButton.outline(
|
||||
icon: const Icon(SpotubeIcons.queueAdd),
|
||||
onPressed: onAddToQueuePressed,
|
||||
enabled: !isLoading,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Tooltip(
|
||||
tooltip: TooltipContainer(child: Text(context.l10n.play)),
|
||||
child: IconButton.secondary(
|
||||
icon: switch ((isLoading, isPlaying)) {
|
||||
(true, _) => const CircularProgressIndicator(
|
||||
size: 22,
|
||||
),
|
||||
(false, false) => const Icon(SpotubeIcons.play),
|
||||
(false, true) => const Icon(SpotubeIcons.pause)
|
||||
},
|
||||
onPressed: onPlaybuttonPressed,
|
||||
enabled: !isLoading,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
enabled: !isLoading,
|
||||
onPressed: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title),
|
||||
if (cleanDescription.isNotEmpty)
|
||||
Text(
|
||||
description!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).xSmall().muted(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
157
lib/components/playbutton_view/playbutton_view.dart
Normal file
157
lib/components/playbutton_view/playbutton_view.dart
Normal file
@ -0,0 +1,157 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
|
||||
import 'package:spotube/components/waypoint.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
const _dummyPlaybuttonCard = PlaybuttonCard(
|
||||
imageUrl: 'https://placehold.co/150x150.png',
|
||||
isLoading: false,
|
||||
isPlaying: false,
|
||||
title: "Playbutton",
|
||||
description: "A really cool playbutton",
|
||||
isOwner: false,
|
||||
);
|
||||
|
||||
const _dummyPlaybuttonTile = PlaybuttonTile(
|
||||
imageUrl: 'https://placehold.co/150x150.png',
|
||||
isLoading: false,
|
||||
isPlaying: false,
|
||||
title: "Playbutton",
|
||||
description: "A really cool playbutton",
|
||||
isOwner: false,
|
||||
);
|
||||
|
||||
/// A [PlaybuttonCard] grid/list view (selectable) sliver widget
|
||||
/// with support for infinite scrolling
|
||||
class PlaybuttonView extends StatelessWidget {
|
||||
final int itemCount;
|
||||
final Widget Function(BuildContext context, int index) gridItemBuilder;
|
||||
final Widget Function(BuildContext context, int index) listItemBuilder;
|
||||
final bool hasMore;
|
||||
final bool isLoading;
|
||||
final VoidCallback onRequestMore;
|
||||
final ScrollController controller;
|
||||
|
||||
const PlaybuttonView({
|
||||
super.key,
|
||||
required this.itemCount,
|
||||
required this.gridItemBuilder,
|
||||
required this.listItemBuilder,
|
||||
required this.hasMore,
|
||||
required this.isLoading,
|
||||
required this.onRequestMore,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverLayoutBuilder(
|
||||
builder: (context, constrains) => HookBuilder(builder: (context) {
|
||||
final isGrid = useState(constrains.mdAndUp);
|
||||
final hasUserInteracted = useRef(false);
|
||||
|
||||
useEffect(() {
|
||||
if (hasUserInteracted.value) return null;
|
||||
if (isGrid.value != constrains.mdAndUp) {
|
||||
isGrid.value = constrains.mdAndUp;
|
||||
}
|
||||
return null;
|
||||
}, [constrains]);
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Toggle(
|
||||
value: isGrid.value,
|
||||
style:
|
||||
const ButtonStyle.outline(density: ButtonDensity.icon),
|
||||
onChanged: (value) {
|
||||
isGrid.value = value;
|
||||
hasUserInteracted.value = true;
|
||||
},
|
||||
child: const Icon(SpotubeIcons.grid),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Toggle(
|
||||
value: !isGrid.value,
|
||||
style:
|
||||
const ButtonStyle.outline(density: ButtonDensity.icon),
|
||||
onChanged: (value) {
|
||||
isGrid.value = !value;
|
||||
hasUserInteracted.value = true;
|
||||
},
|
||||
child: const Icon(SpotubeIcons.list),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(10),
|
||||
// Toggle between grid and list view
|
||||
switch ((isGrid.value, isLoading)) {
|
||||
(true, _) => SliverGrid.builder(
|
||||
itemCount: isLoading ? 6 : itemCount + 1,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 150,
|
||||
mainAxisExtent: 225,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (isLoading) {
|
||||
return const Skeletonizer(
|
||||
enabled: true,
|
||||
child: _dummyPlaybuttonCard,
|
||||
);
|
||||
}
|
||||
|
||||
if (index == itemCount) {
|
||||
if (!hasMore) return const SizedBox.shrink();
|
||||
return Waypoint(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: onRequestMore,
|
||||
child: const Skeletonizer(
|
||||
enabled: true,
|
||||
child: _dummyPlaybuttonCard,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return gridItemBuilder(context, index);
|
||||
},
|
||||
),
|
||||
(false, true) => Skeletonizer.sliver(
|
||||
enabled: true,
|
||||
child: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _dummyPlaybuttonTile,
|
||||
childCount: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
(false, false) => SliverInfiniteList(
|
||||
itemCount: itemCount,
|
||||
loadingBuilder: (context) => const Skeletonizer(
|
||||
enabled: true,
|
||||
child: _dummyPlaybuttonTile,
|
||||
),
|
||||
itemBuilder: listItemBuilder,
|
||||
onFetchData: onRequestMore,
|
||||
hasReachedMax: !hasMore,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
}
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -401,5 +401,6 @@
|
||||
"export_cache_files": "Export Cached Files",
|
||||
"found_n_files": "Found {count} files",
|
||||
"export_cache_confirmation": "Do you want to export these files to",
|
||||
"exported_n_out_of_m_files": "Exported {filesExported} out of {files} files"
|
||||
"exported_n_out_of_m_files": "Exported {filesExported} out of {files} files",
|
||||
"undo": "Undo"
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||
import 'package:spotube/components/playbutton_card.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
@ -24,10 +25,16 @@ extension FormattedAlbumType on AlbumType {
|
||||
|
||||
class AlbumCard extends HookConsumerWidget {
|
||||
final AlbumSimple album;
|
||||
final bool _isTile;
|
||||
const AlbumCard(
|
||||
this.album, {
|
||||
super.key,
|
||||
});
|
||||
}) : _isTile = false;
|
||||
|
||||
const AlbumCard.tile(
|
||||
this.album, {
|
||||
super.key,
|
||||
}) : _isTile = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -45,8 +52,6 @@ class AlbumCard extends HookConsumerWidget {
|
||||
|
||||
final updating = useState(false);
|
||||
|
||||
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
|
||||
|
||||
Future<List<Track>> fetchAllTrack() async {
|
||||
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
||||
return album.tracks!.map((track) => track.asTrack(album)).toList();
|
||||
@ -55,18 +60,15 @@ class AlbumCard extends HookConsumerWidget {
|
||||
return ref.read(albumTracksProvider(album).notifier).fetchAll();
|
||||
}
|
||||
|
||||
return PlaybuttonCard(
|
||||
imageUrl: album.images.asUrlString(
|
||||
var imageUrl = album.images.asUrlString(
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||
isPlaying: isPlaylistPlaying,
|
||||
isLoading:
|
||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
|
||||
title: album.name!,
|
||||
description:
|
||||
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
||||
onTap: () {
|
||||
);
|
||||
var isLoading =
|
||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||
var description =
|
||||
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}";
|
||||
|
||||
void onTap() {
|
||||
ServiceUtils.pushNamed(
|
||||
context,
|
||||
AlbumPage.name,
|
||||
@ -75,8 +77,9 @@ class AlbumCard extends HookConsumerWidget {
|
||||
},
|
||||
extra: album,
|
||||
);
|
||||
},
|
||||
onPlaybuttonPressed: () async {
|
||||
}
|
||||
|
||||
void onPlaybuttonPressed() async {
|
||||
updating.value = true;
|
||||
try {
|
||||
if (isPlaylistPlaying) {
|
||||
@ -104,8 +107,9 @@ class AlbumCard extends HookConsumerWidget {
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
},
|
||||
onAddToQueuePressed: () async {
|
||||
}
|
||||
|
||||
void onAddToQueuePressed() async {
|
||||
if (isPlaylistPlaying) {
|
||||
return;
|
||||
}
|
||||
@ -119,24 +123,53 @@ class AlbumCard extends HookConsumerWidget {
|
||||
playlistNotifier.addCollection(album.id!);
|
||||
historyNotifier.addAlbums([album]);
|
||||
if (context.mounted) {
|
||||
final snackbar = SnackBar(
|
||||
showToast(
|
||||
context: context,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
child: Basic(
|
||||
content: Text(
|
||||
context.l10n.added_to_queue(fetchedTracks.length),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: "Undo",
|
||||
trailing: Button.outline(
|
||||
child: Text(context.l10n.undo),
|
||||
onPressed: () {
|
||||
playlistNotifier
|
||||
.removeTracks(fetchedTracks.map((e) => e.id!));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
scaffoldMessenger?.showSnackBar(snackbar);
|
||||
}
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (_isTile) {
|
||||
return PlaybuttonTile(
|
||||
imageUrl: imageUrl,
|
||||
isPlaying: isPlaylistPlaying,
|
||||
isLoading: isLoading,
|
||||
title: album.name!,
|
||||
description: description,
|
||||
onTap: onTap,
|
||||
onPlaybuttonPressed: onPlaybuttonPressed,
|
||||
onAddToQueuePressed: onAddToQueuePressed,
|
||||
);
|
||||
}
|
||||
|
||||
return PlaybuttonCard(
|
||||
imageUrl: imageUrl,
|
||||
isPlaying: isPlaylistPlaying,
|
||||
isLoading: isLoading,
|
||||
title: album.name!,
|
||||
description: description,
|
||||
onTap: onTap,
|
||||
onPlaybuttonPressed: onPlaybuttonPressed,
|
||||
onAddToQueuePressed: onAddToQueuePressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,12 @@ import 'package:collection/collection.dart';
|
||||
import 'package:fuzzywuzzy/fuzzywuzzy.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/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/modules/album/album_card.dart';
|
||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/waypoint.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
@ -78,39 +76,17 @@ class UserAlbums extends HookConsumerWidget {
|
||||
const SliverGap(10),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
sliver: SliverGrid.builder(
|
||||
itemCount: albums.isEmpty ? 6 : albums.length + 1,
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 150,
|
||||
mainAxisExtent: 225,
|
||||
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(
|
||||
sliver: PlaybuttonView(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: albumsQueryNotifier.fetchMore,
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: AlbumCard(FakeData.albumSimple),
|
||||
itemCount: albums.length,
|
||||
hasMore: albumsQuery.asData?.value.hasMore == true,
|
||||
isLoading: albumsQuery.isLoading,
|
||||
onRequestMore: albumsQueryNotifier.fetchMore,
|
||||
gridItemBuilder: (context, index) => AlbumCard(
|
||||
albums[index],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: albumsQuery.isLoading,
|
||||
child: AlbumCard(
|
||||
albums.elementAtOrNull(index) ?? FakeData.albumSimple,
|
||||
),
|
||||
);
|
||||
},
|
||||
listItemBuilder: (context, index) =>
|
||||
AlbumCard.tile(albums[index]),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -5,16 +5,14 @@ import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image;
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||
import 'package:spotube/components/waypoint.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
@ -127,35 +125,17 @@ class UserPlaylists extends HookConsumerWidget {
|
||||
const SliverGap(10),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
sliver: SliverGrid.builder(
|
||||
itemCount: playlists.isEmpty ? 6 : playlists.length + 1,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 150,
|
||||
mainAxisExtent: 225,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (playlists.isNotEmpty && index == playlists.length) {
|
||||
if (playlistsQuery.asData?.value.hasMore != true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Waypoint(
|
||||
sliver: PlaybuttonView(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: playlistsQueryNotifier.fetchMore,
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: PlaylistCard(FakeData.playlistSimple),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return PlaylistCard(
|
||||
playlists.elementAtOrNull(index) ??
|
||||
FakeData.playlistSimple,
|
||||
);
|
||||
hasMore: playlistsQuery.asData?.value.hasMore == true,
|
||||
isLoading: playlistsQuery.isLoading,
|
||||
onRequestMore: playlistsQueryNotifier.fetchMore,
|
||||
itemCount: playlists.length,
|
||||
gridItemBuilder: (context, index) {
|
||||
return PlaylistCard(playlists[index]);
|
||||
},
|
||||
listItemBuilder: (context, index) {
|
||||
return PlaylistCard.tile(playlists[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -79,7 +79,7 @@ class PlayerActions extends HookConsumerWidget {
|
||||
children: [
|
||||
if (showQueue)
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.queue),
|
||||
tooltip: TooltipContainer(child: Text(context.l10n.queue)),
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.queue),
|
||||
enabled: playlist.activeTrack != null,
|
||||
@ -115,7 +115,8 @@ class PlayerActions extends HookConsumerWidget {
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.alternative_track_sources),
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.alternative_track_sources)),
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.alternativeRoute),
|
||||
onPressed: playlist.activeTrack != null
|
||||
@ -147,7 +148,8 @@ class PlayerActions extends HookConsumerWidget {
|
||||
)
|
||||
else
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.download_track),
|
||||
tooltip:
|
||||
TooltipContainer(child: Text(context.l10n.download_track)),
|
||||
child: IconButton.ghost(
|
||||
icon: Icon(
|
||||
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
||||
|
@ -84,7 +84,8 @@ class PlayerControls extends HookConsumerWidget {
|
||||
return Column(
|
||||
children: [
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.slide_to_seek),
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.slide_to_seek)),
|
||||
child: Slider(
|
||||
value:
|
||||
SliderValue.single(progress.value.toDouble()),
|
||||
@ -132,11 +133,13 @@ class PlayerControls extends HookConsumerWidget {
|
||||
final shuffled = ref
|
||||
.watch(audioPlayerProvider.select((s) => s.shuffled));
|
||||
return Tooltip(
|
||||
tooltip: Text(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(
|
||||
shuffled
|
||||
? context.l10n.unshuffle_playlist
|
||||
: context.l10n.shuffle_playlist,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(SpotubeIcons.shuffle),
|
||||
variance: shuffled
|
||||
@ -155,7 +158,8 @@ class PlayerControls extends HookConsumerWidget {
|
||||
);
|
||||
}),
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.previous_track),
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.previous_track)),
|
||||
child: IconButton.ghost(
|
||||
enabled: !isFetchingActiveTrack,
|
||||
icon: const Icon(SpotubeIcons.skipBack),
|
||||
@ -163,11 +167,13 @@ class PlayerControls extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
tooltip: Text(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(
|
||||
playing
|
||||
? context.l10n.pause_playback
|
||||
: context.l10n.resume_playback,
|
||||
),
|
||||
),
|
||||
child: IconButton.primary(
|
||||
shape: ButtonShape.circle,
|
||||
icon: isFetchingActiveTrack
|
||||
@ -188,7 +194,8 @@ class PlayerControls extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.next_track),
|
||||
tooltip:
|
||||
TooltipContainer(child: Text(context.l10n.next_track)),
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.skipForward),
|
||||
onPressed:
|
||||
@ -200,13 +207,15 @@ class PlayerControls extends HookConsumerWidget {
|
||||
.watch(audioPlayerProvider.select((s) => s.loopMode));
|
||||
|
||||
return Tooltip(
|
||||
tooltip: Text(
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(
|
||||
loopMode == PlaylistMode.single
|
||||
? context.l10n.loop_track
|
||||
: loopMode == PlaylistMode.loop
|
||||
? context.l10n.repeat_playlist
|
||||
: "",
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
loopMode == PlaylistMode.single
|
||||
|
@ -160,7 +160,8 @@ class PlayerQueue extends HookConsumerWidget {
|
||||
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
||||
const SizedBox(width: 10),
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.clear_all),
|
||||
tooltip: TooltipContainer(
|
||||
child: Text(context.l10n.clear_all)),
|
||||
child: IconButton.outline(
|
||||
icon: const Icon(SpotubeIcons.playlistRemove),
|
||||
onPressed: () {
|
||||
|
@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||
import 'package:spotube/components/playbutton_card.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/models/connect/connect.dart';
|
||||
@ -18,10 +19,18 @@ import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class PlaylistCard extends HookConsumerWidget {
|
||||
final PlaylistSimple playlist;
|
||||
final bool _isTile;
|
||||
|
||||
const PlaylistCard(
|
||||
this.playlist, {
|
||||
super.key,
|
||||
});
|
||||
}) : _isTile = false;
|
||||
|
||||
const PlaylistCard.tile(
|
||||
this.playlist, {
|
||||
super.key,
|
||||
}) : _isTile = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlistQueue = ref.watch(audioPlayerProvider);
|
||||
@ -60,18 +69,7 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
|
||||
}
|
||||
|
||||
return PlaybuttonCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||
title: playlist.name!,
|
||||
description: playlist.description,
|
||||
imageUrl: playlist.images.asUrlString(
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
),
|
||||
isPlaying: isPlaylistPlaying,
|
||||
isLoading: (isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
|
||||
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
||||
me.asData?.value.id != null,
|
||||
onTap: () {
|
||||
void onTap() {
|
||||
ServiceUtils.pushNamed(
|
||||
context,
|
||||
PlaylistPage.name,
|
||||
@ -80,8 +78,9 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
},
|
||||
extra: playlist,
|
||||
);
|
||||
},
|
||||
onPlaybuttonPressed: () async {
|
||||
}
|
||||
|
||||
void onPlaybuttonPressed() async {
|
||||
try {
|
||||
updating.value = true;
|
||||
if (isPlaylistPlaying && playing) {
|
||||
@ -119,8 +118,9 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
updating.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
onAddToQueuePressed: () async {
|
||||
}
|
||||
|
||||
void onAddToQueuePressed() async {
|
||||
updating.value = true;
|
||||
try {
|
||||
if (isPlaylistPlaying) return;
|
||||
@ -133,23 +133,64 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
playlistNotifier.addCollection(playlist.id!);
|
||||
historyNotifier.addPlaylists([playlist]);
|
||||
if (context.mounted) {
|
||||
final snackbar = SnackBar(
|
||||
content: Text(context.l10n
|
||||
.added_num_tracks_to_queue(fetchedInitialTracks.length)),
|
||||
action: SnackBarAction(
|
||||
label: "Undo",
|
||||
showToast(
|
||||
context: context,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
child: Basic(
|
||||
content: Text(
|
||||
context.l10n
|
||||
.added_num_tracks_to_queue(fetchedInitialTracks.length),
|
||||
),
|
||||
trailing: Button.outline(
|
||||
child: Text(context.l10n.undo),
|
||||
onPressed: () {
|
||||
playlistNotifier
|
||||
.removeTracks(fetchedInitialTracks.map((e) => e.id!));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(snackbar);
|
||||
}
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
final imageUrl = playlist.images.asUrlString(
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
);
|
||||
final isLoading =
|
||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||
final isOwner = playlist.owner?.id == me.asData?.value.id &&
|
||||
me.asData?.value.id != null;
|
||||
|
||||
if (_isTile) {
|
||||
return PlaybuttonTile(
|
||||
title: playlist.name!,
|
||||
description: playlist.description,
|
||||
imageUrl: imageUrl,
|
||||
isPlaying: isPlaylistPlaying,
|
||||
isLoading: isLoading,
|
||||
isOwner: isOwner,
|
||||
onTap: onTap,
|
||||
onPlaybuttonPressed: onPlaybuttonPressed,
|
||||
onAddToQueuePressed: onAddToQueuePressed,
|
||||
);
|
||||
}
|
||||
|
||||
return PlaybuttonCard(
|
||||
title: playlist.name!,
|
||||
description: playlist.description,
|
||||
imageUrl: imageUrl,
|
||||
isPlaying: isPlaylistPlaying,
|
||||
isLoading: isLoading,
|
||||
isOwner: isOwner,
|
||||
onTap: onTap,
|
||||
onPlaybuttonPressed: onPlaybuttonPressed,
|
||||
onAddToQueuePressed: onAddToQueuePressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,8 @@ class BottomPlayer extends HookConsumerWidget {
|
||||
PlayerActions(
|
||||
extraActions: [
|
||||
Tooltip(
|
||||
tooltip: Text(context.l10n.mini_player),
|
||||
tooltip:
|
||||
TooltipContainer(child: Text(context.l10n.mini_player)),
|
||||
child: IconButton(
|
||||
variance: ButtonVariance.ghost,
|
||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||
|
@ -1,12 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/modules/album/album_card.dart';
|
||||
import 'package:spotube/modules/artist/artist_card.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/provider/spotify/views/home_section.dart';
|
||||
|
||||
class HomeFeedSectionPage extends HookConsumerWidget {
|
||||
@ -19,39 +20,63 @@ class HomeFeedSectionPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final homeFeedSection = ref.watch(homeSectionViewProvider(sectionUri));
|
||||
final section = homeFeedSection.asData?.value ?? FakeData.feedSection;
|
||||
final controller = useScrollController();
|
||||
final isArtist = section.items.every((item) => item.artist != null);
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: homeFeedSection.isLoading,
|
||||
child: Scaffold(
|
||||
appBar: TitleBar(
|
||||
headers: [
|
||||
TitleBar(
|
||||
title: Text(section.title ?? ""),
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: CustomScrollView(
|
||||
)
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constrains) {
|
||||
return SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
if (isArtist)
|
||||
SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||
mainAxisExtent: 250,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: section.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = section.items[index];
|
||||
|
||||
return ArtistCard(item.artist!.asArtist);
|
||||
},
|
||||
)
|
||||
else
|
||||
PlaybuttonView(
|
||||
controller: controller,
|
||||
itemCount: section.items.length,
|
||||
hasMore: false,
|
||||
isLoading: false,
|
||||
onRequestMore: () => {},
|
||||
listItemBuilder: (context, index) {
|
||||
final item = section.items[index];
|
||||
if (item.album != null) {
|
||||
return AlbumCard.tile(item.album!.asAlbum);
|
||||
}
|
||||
if (item.playlist != null) {
|
||||
return PlaylistCard.tile(item.playlist!.asPlaylist);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
gridItemBuilder: (context, index) {
|
||||
final item = section.items[index];
|
||||
if (item.album != null) {
|
||||
return AlbumCard(item.album!.asAlbum);
|
||||
} else if (item.artist != null) {
|
||||
return ArtistCard(item.artist!.asArtist);
|
||||
} else if (item.playlist != null) {
|
||||
}
|
||||
if (item.playlist != null) {
|
||||
return PlaylistCard(item.playlist!.asPlaylist);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
@ -62,6 +87,7 @@ class HomeFeedSectionPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' show CollapseMode, FlexibleSpaceBar;
|
||||
import 'package:flutter_hooks/flutter_hooks.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';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
|
||||
import 'package:spotify/spotify.dart' hide Offset;
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/components/button/back_button.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||
import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
|
||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/components/waypoint.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
@ -39,33 +40,37 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: kIsDesktop
|
||||
? const TitleBar(
|
||||
leading: [BackButton(color: Colors.white)],
|
||||
headers: [
|
||||
if (kIsDesktop)
|
||||
const TitleBar(
|
||||
leading: [
|
||||
BackButton(),
|
||||
],
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
surfaceOpacity: 0,
|
||||
surfaceBlur: 0,
|
||||
)
|
||||
: null,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: DecoratedBox(
|
||||
],
|
||||
floatingHeader: true,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(category.icons!.first.url!),
|
||||
alignment: Alignment.topCenter,
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(0.5),
|
||||
BlendMode.darken,
|
||||
),
|
||||
repeat: ImageRepeat.noRepeat,
|
||||
matchTextDirection: true,
|
||||
),
|
||||
),
|
||||
child: SurfaceCard(
|
||||
borderRadius: BorderRadius.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
automaticallyImplyLeading: kIsMobile,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: kIsMobile ? const BackButton() : null,
|
||||
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
||||
title: const Text(""),
|
||||
backgroundColor: Colors.transparent,
|
||||
@ -73,25 +78,25 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
centerTitle: kIsDesktop,
|
||||
title: Text(
|
||||
category.name!,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
style: context.theme.typography.h3.copyWith(
|
||||
color: Colors.white,
|
||||
letterSpacing: 3,
|
||||
shadows: [
|
||||
const Shadow(
|
||||
offset: Offset(-1.5, -1.5),
|
||||
color: Colors.black54,
|
||||
Shadow(
|
||||
offset: const Offset(-1.5, -1.5),
|
||||
color: Colors.black.withAlpha(138),
|
||||
),
|
||||
const Shadow(
|
||||
offset: Offset(1.5, -1.5),
|
||||
color: Colors.black54,
|
||||
Shadow(
|
||||
offset: const Offset(1.5, -1.5),
|
||||
color: Colors.black.withAlpha(138),
|
||||
),
|
||||
const Shadow(
|
||||
offset: Offset(1.5, 1.5),
|
||||
color: Colors.black54,
|
||||
Shadow(
|
||||
offset: const Offset(1.5, 1.5),
|
||||
color: Colors.black.withAlpha(138),
|
||||
),
|
||||
const Shadow(
|
||||
offset: Offset(-1.5, 1.5),
|
||||
color: Colors.black54,
|
||||
Shadow(
|
||||
offset: const Offset(-1.5, 1.5),
|
||||
color: Colors.black.withAlpha(138),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -106,51 +111,16 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: mediaQuery.mdAndDown ? 12 : 24,
|
||||
),
|
||||
sliver: playlists.asData?.value.items.isNotEmpty != true
|
||||
? Skeletonizer.sliver(
|
||||
child: SliverToBoxAdapter(
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: List.generate(
|
||||
6,
|
||||
(index) => PlaylistCard(FakeData.playlist),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 190,
|
||||
mainAxisExtent: mediaQuery.mdAndDown ? 225 : 250,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount:
|
||||
(playlists.asData?.value.items.length ?? 0) + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlists.asData?.value.items
|
||||
.elementAtOrNull(index);
|
||||
|
||||
if (playlist == null) {
|
||||
if (playlists.asData?.value.hasMore == false) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Skeletonizer(
|
||||
enabled: true,
|
||||
child: Waypoint(
|
||||
sliver: PlaybuttonView(
|
||||
controller: scrollController,
|
||||
isGrid: true,
|
||||
onTouchEdge: playlistsNotifier.fetchMore,
|
||||
child: PlaylistCard(FakeData.playlist),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Skeleton.keep(
|
||||
child: PlaylistCard(playlist),
|
||||
);
|
||||
},
|
||||
itemCount: playlists.asData?.value.items.length ?? 0,
|
||||
isLoading: playlists.isLoading,
|
||||
hasMore: playlists.asData?.value.hasMore == true,
|
||||
onRequestMore: playlistsNotifier.fetchMore,
|
||||
listItemBuilder: (context, index) =>
|
||||
PlaylistCard.tile(playlists.asData!.value.items[index]),
|
||||
gridItemBuilder: (context, index) =>
|
||||
PlaylistCard(playlists.asData!.value.items[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -158,6 +128,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,105 @@
|
||||
{}
|
||||
{
|
||||
"ar": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"ka": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"th": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
"undo"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"undo"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user