mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55: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 open = FeatherIcons.externalLink;
|
||||||
static const radioChecked = Icons.radio_button_on_rounded;
|
static const radioChecked = Icons.radio_button_on_rounded;
|
||||||
static const radioUnchecked = Icons.radio_button_off_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) {
|
if (mediaQuery.mdAndUp) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
tooltip: Text(tooltip ?? ''),
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(tooltip ?? ''),
|
||||||
|
),
|
||||||
child: IconButton.ghost(
|
child: IconButton.ghost(
|
||||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -162,7 +164,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
|
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
tooltip: Text(tooltip ?? ''),
|
tooltip: TooltipContainer(child: Text(tooltip ?? '')),
|
||||||
child: Button(
|
child: Button(
|
||||||
onPressed: () => showDropdownMenu(context, Offset.zero),
|
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||||
style: const ButtonStyle.ghost(),
|
style: const ButtonStyle.ghost(),
|
||||||
@ -172,7 +174,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
tooltip: Text(tooltip ?? ''),
|
tooltip: TooltipContainer(child: Text(tooltip ?? '')),
|
||||||
child: IconButton.ghost(
|
child: IconButton.ghost(
|
||||||
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
icon: icon ?? const Icon(SpotubeIcons.moreVertical),
|
||||||
onPressed: () => showDropdownMenu(context, Offset.zero),
|
onPressed: () => showDropdownMenu(context, Offset.zero),
|
||||||
|
@ -2,13 +2,19 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|
||||||
class BackButton extends StatelessWidget {
|
class BackButton extends StatelessWidget {
|
||||||
const BackButton({super.key});
|
final Color? color;
|
||||||
|
const BackButton({
|
||||||
|
super.key,
|
||||||
|
this.color,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton.ghost(
|
return IconButton.ghost(
|
||||||
size: const ButtonSize(.9),
|
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(),
|
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/album/album_card.dart';
|
||||||
import 'package:spotube/modules/artist/artist_card.dart';
|
import 'package:spotube/modules/artist/artist_card.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_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';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
||||||
@ -38,12 +37,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final height = useBreakpointValue<double>(
|
final isArtist = items.every((s) => s is Artist);
|
||||||
xs: 226,
|
|
||||||
sm: 226,
|
|
||||||
md: 236,
|
|
||||||
others: 266,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@ -64,7 +58,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: height,
|
height: isArtist ? 250 : 225,
|
||||||
child: NotificationListener(
|
child: NotificationListener(
|
||||||
// disable multiple scrollbar to use this
|
// disable multiple scrollbar to use this
|
||||||
onNotification: (notification) => true,
|
onNotification: (notification) => true,
|
||||||
@ -88,7 +82,9 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
onFetchData: onFetchMore,
|
onFetchData: onFetchMore,
|
||||||
loadingBuilder: (context) => Skeletonizer(
|
loadingBuilder: (context) => Skeletonizer(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
child: AlbumCard(FakeData.albumSimple),
|
child: isArtist
|
||||||
|
? ArtistCard(FakeData.artist)
|
||||||
|
: AlbumCard(FakeData.albumSimple),
|
||||||
),
|
),
|
||||||
isLoading: isLoadingNextPage,
|
isLoading: isLoadingNextPage,
|
||||||
hasReachedMax: !hasNextPage,
|
hasReachedMax: !hasNextPage,
|
||||||
@ -100,11 +96,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
PlaylistSimple() =>
|
PlaylistSimple() =>
|
||||||
PlaylistCard(item as PlaylistSimple),
|
PlaylistCard(item as PlaylistSimple),
|
||||||
AlbumSimple() => AlbumCard(item as AlbumSimple),
|
AlbumSimple() => AlbumCard(item as AlbumSimple),
|
||||||
Artist() => Padding(
|
Artist() => ArtistCard(item as Artist),
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12.0),
|
|
||||||
child: ArtistCard(item as Artist),
|
|
||||||
),
|
|
||||||
_ => const SizedBox.shrink(),
|
_ => const SizedBox.shrink(),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
|
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class PlaybuttonCard extends HookWidget {
|
class PlaybuttonCard extends StatelessWidget {
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
final void Function()? onPlaybuttonPressed;
|
final void Function()? onPlaybuttonPressed;
|
||||||
final void Function()? onAddToQueuePressed;
|
final void Function()? onAddToQueuePressed;
|
||||||
final String? description;
|
final String? description;
|
||||||
final EdgeInsetsGeometry? margin;
|
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final bool isPlaying;
|
final bool isPlaying;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
@ -23,7 +21,6 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
required this.isPlaying,
|
required this.isPlaying,
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.margin,
|
|
||||||
this.description,
|
this.description,
|
||||||
this.onPlaybuttonPressed,
|
this.onPlaybuttonPressed,
|
||||||
this.onAddToQueuePressed,
|
this.onAddToQueuePressed,
|
||||||
@ -56,13 +53,16 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
AnimatedScale(
|
AnimatedScale(
|
||||||
curve: Curves.easeOutBack,
|
curve: Curves.easeOutBack,
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
scale: states.contains(WidgetState.hovered) || kIsMobile
|
scale: (states.contains(WidgetState.hovered) ||
|
||||||
|
kIsMobile) &&
|
||||||
|
!isLoading
|
||||||
? 1
|
? 1
|
||||||
: 0.7,
|
: 0.7,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
opacity:
|
opacity: (states.contains(WidgetState.hovered) ||
|
||||||
states.contains(WidgetState.hovered) || kIsMobile
|
kIsMobile) &&
|
||||||
|
!isLoading
|
||||||
? 1
|
? 1
|
||||||
: 0,
|
: 0,
|
||||||
child: IconButton.secondary(
|
child: IconButton.secondary(
|
||||||
@ -76,17 +76,29 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
AnimatedScale(
|
AnimatedScale(
|
||||||
curve: Curves.easeOutBack,
|
curve: Curves.easeOutBack,
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
scale: states.contains(WidgetState.hovered) || kIsMobile
|
scale: states.contains(WidgetState.hovered) ||
|
||||||
|
kIsMobile ||
|
||||||
|
isPlaying ||
|
||||||
|
isLoading
|
||||||
? 1
|
? 1
|
||||||
: 0.7,
|
: 0.7,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
opacity:
|
opacity: states.contains(WidgetState.hovered) ||
|
||||||
states.contains(WidgetState.hovered) || kIsMobile
|
kIsMobile ||
|
||||||
|
isPlaying ||
|
||||||
|
isLoading
|
||||||
? 1
|
? 1
|
||||||
: 0,
|
: 0,
|
||||||
child: IconButton.secondary(
|
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,
|
onPressed: onPlaybuttonPressed,
|
||||||
size: ButtonSize.small,
|
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(
|
title: Tooltip(
|
||||||
tooltip: Text(title),
|
tooltip: TooltipContainer(child: Text(title)),
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
maxLines: 1,
|
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",
|
"export_cache_files": "Export Cached Files",
|
||||||
"found_n_files": "Found {count} files",
|
"found_n_files": "Found {count} files",
|
||||||
"export_cache_confirmation": "Do you want to export these files to",
|
"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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.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/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
@ -24,10 +25,16 @@ extension FormattedAlbumType on AlbumType {
|
|||||||
|
|
||||||
class AlbumCard extends HookConsumerWidget {
|
class AlbumCard extends HookConsumerWidget {
|
||||||
final AlbumSimple album;
|
final AlbumSimple album;
|
||||||
|
final bool _isTile;
|
||||||
const AlbumCard(
|
const AlbumCard(
|
||||||
this.album, {
|
this.album, {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
}) : _isTile = false;
|
||||||
|
|
||||||
|
const AlbumCard.tile(
|
||||||
|
this.album, {
|
||||||
|
super.key,
|
||||||
|
}) : _isTile = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
@ -45,8 +52,6 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
|
|
||||||
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
|
|
||||||
|
|
||||||
Future<List<Track>> fetchAllTrack() async {
|
Future<List<Track>> fetchAllTrack() async {
|
||||||
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
||||||
return album.tracks!.map((track) => track.asTrack(album)).toList();
|
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 ref.read(albumTracksProvider(album).notifier).fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlaybuttonCard(
|
var imageUrl = album.images.asUrlString(
|
||||||
imageUrl: album.images.asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
);
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
var isLoading =
|
||||||
isPlaying: isPlaylistPlaying,
|
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||||
isLoading:
|
var description =
|
||||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value,
|
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}";
|
||||||
title: album.name!,
|
|
||||||
description:
|
void onTap() {
|
||||||
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
|
||||||
onTap: () {
|
|
||||||
ServiceUtils.pushNamed(
|
ServiceUtils.pushNamed(
|
||||||
context,
|
context,
|
||||||
AlbumPage.name,
|
AlbumPage.name,
|
||||||
@ -75,8 +77,9 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
extra: album,
|
extra: album,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
onPlaybuttonPressed: () async {
|
|
||||||
|
void onPlaybuttonPressed() async {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
try {
|
try {
|
||||||
if (isPlaylistPlaying) {
|
if (isPlaylistPlaying) {
|
||||||
@ -104,8 +107,9 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onAddToQueuePressed: () async {
|
|
||||||
|
void onAddToQueuePressed() async {
|
||||||
if (isPlaylistPlaying) {
|
if (isPlaylistPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -119,24 +123,53 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
playlistNotifier.addCollection(album.id!);
|
playlistNotifier.addCollection(album.id!);
|
||||||
historyNotifier.addAlbums([album]);
|
historyNotifier.addAlbums([album]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
showToast(
|
||||||
|
context: context,
|
||||||
|
builder: (context, overlay) {
|
||||||
|
return SurfaceCard(
|
||||||
|
child: Basic(
|
||||||
content: Text(
|
content: Text(
|
||||||
context.l10n.added_to_queue(fetchedTracks.length),
|
context.l10n.added_to_queue(fetchedTracks.length),
|
||||||
),
|
),
|
||||||
action: SnackBarAction(
|
trailing: Button.outline(
|
||||||
label: "Undo",
|
child: Text(context.l10n.undo),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier
|
playlistNotifier
|
||||||
.removeTracks(fetchedTracks.map((e) => e.id!));
|
.removeTracks(fetchedTracks.map((e) => e.id!));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
scaffoldMessenger?.showSnackBar(snackbar);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
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:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/playbutton_view/playbutton_view.dart';
|
||||||
import 'package:spotube/modules/album/album_card.dart';
|
import 'package:spotube/modules/album/album_card.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||||
import 'package:spotube/components/waypoint.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
@ -78,39 +76,17 @@ class UserAlbums extends HookConsumerWidget {
|
|||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
sliver: SliverGrid.builder(
|
sliver: PlaybuttonView(
|
||||||
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(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
isGrid: true,
|
itemCount: albums.length,
|
||||||
onTouchEdge: albumsQueryNotifier.fetchMore,
|
hasMore: albumsQuery.asData?.value.hasMore == true,
|
||||||
child: Skeletonizer(
|
isLoading: albumsQuery.isLoading,
|
||||||
enabled: true,
|
onRequestMore: albumsQueryNotifier.fetchMore,
|
||||||
child: AlbumCard(FakeData.albumSimple),
|
gridItemBuilder: (context, index) => AlbumCard(
|
||||||
|
albums[index],
|
||||||
),
|
),
|
||||||
);
|
listItemBuilder: (context, index) =>
|
||||||
}
|
AlbumCard.tile(albums[index]),
|
||||||
|
|
||||||
return Skeletonizer(
|
|
||||||
enabled: albumsQuery.isLoading,
|
|
||||||
child: AlbumCard(
|
|
||||||
albums.elementAtOrNull(index) ?? FakeData.albumSimple,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -5,16 +5,14 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image;
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image;
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.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/playbutton_view/playbutton_view.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||||
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||||
import 'package:spotube/components/waypoint.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
@ -127,35 +125,17 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
sliver: SliverGrid.builder(
|
sliver: PlaybuttonView(
|
||||||
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(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
isGrid: true,
|
hasMore: playlistsQuery.asData?.value.hasMore == true,
|
||||||
onTouchEdge: playlistsQueryNotifier.fetchMore,
|
isLoading: playlistsQuery.isLoading,
|
||||||
child: Skeletonizer(
|
onRequestMore: playlistsQueryNotifier.fetchMore,
|
||||||
enabled: true,
|
itemCount: playlists.length,
|
||||||
child: PlaylistCard(FakeData.playlistSimple),
|
gridItemBuilder: (context, index) {
|
||||||
),
|
return PlaylistCard(playlists[index]);
|
||||||
);
|
},
|
||||||
}
|
listItemBuilder: (context, index) {
|
||||||
|
return PlaylistCard.tile(playlists[index]);
|
||||||
return PlaylistCard(
|
|
||||||
playlists.elementAtOrNull(index) ??
|
|
||||||
FakeData.playlistSimple,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -79,7 +79,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (showQueue)
|
if (showQueue)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.queue),
|
tooltip: TooltipContainer(child: Text(context.l10n.queue)),
|
||||||
child: IconButton.ghost(
|
child: IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.queue),
|
icon: const Icon(SpotubeIcons.queue),
|
||||||
enabled: playlist.activeTrack != null,
|
enabled: playlist.activeTrack != null,
|
||||||
@ -115,7 +115,8 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (!isLocalTrack)
|
if (!isLocalTrack)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.alternative_track_sources),
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(context.l10n.alternative_track_sources)),
|
||||||
child: IconButton.ghost(
|
child: IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.alternativeRoute),
|
icon: const Icon(SpotubeIcons.alternativeRoute),
|
||||||
onPressed: playlist.activeTrack != null
|
onPressed: playlist.activeTrack != null
|
||||||
@ -147,7 +148,8 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.download_track),
|
tooltip:
|
||||||
|
TooltipContainer(child: Text(context.l10n.download_track)),
|
||||||
child: IconButton.ghost(
|
child: IconButton.ghost(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
||||||
|
@ -84,7 +84,8 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.slide_to_seek),
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(context.l10n.slide_to_seek)),
|
||||||
child: Slider(
|
child: Slider(
|
||||||
value:
|
value:
|
||||||
SliderValue.single(progress.value.toDouble()),
|
SliderValue.single(progress.value.toDouble()),
|
||||||
@ -132,11 +133,13 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
final shuffled = ref
|
final shuffled = ref
|
||||||
.watch(audioPlayerProvider.select((s) => s.shuffled));
|
.watch(audioPlayerProvider.select((s) => s.shuffled));
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
tooltip: Text(
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(
|
||||||
shuffled
|
shuffled
|
||||||
? context.l10n.unshuffle_playlist
|
? context.l10n.unshuffle_playlist
|
||||||
: context.l10n.shuffle_playlist,
|
: context.l10n.shuffle_playlist,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(SpotubeIcons.shuffle),
|
icon: const Icon(SpotubeIcons.shuffle),
|
||||||
variance: shuffled
|
variance: shuffled
|
||||||
@ -155,7 +158,8 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.previous_track),
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(context.l10n.previous_track)),
|
||||||
child: IconButton.ghost(
|
child: IconButton.ghost(
|
||||||
enabled: !isFetchingActiveTrack,
|
enabled: !isFetchingActiveTrack,
|
||||||
icon: const Icon(SpotubeIcons.skipBack),
|
icon: const Icon(SpotubeIcons.skipBack),
|
||||||
@ -163,11 +167,13 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(
|
||||||
playing
|
playing
|
||||||
? context.l10n.pause_playback
|
? context.l10n.pause_playback
|
||||||
: context.l10n.resume_playback,
|
: context.l10n.resume_playback,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
child: IconButton.primary(
|
child: IconButton.primary(
|
||||||
shape: ButtonShape.circle,
|
shape: ButtonShape.circle,
|
||||||
icon: isFetchingActiveTrack
|
icon: isFetchingActiveTrack
|
||||||
@ -188,7 +194,8 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.next_track),
|
tooltip:
|
||||||
|
TooltipContainer(child: Text(context.l10n.next_track)),
|
||||||
child: IconButton.ghost(
|
child: IconButton.ghost(
|
||||||
icon: const Icon(SpotubeIcons.skipForward),
|
icon: const Icon(SpotubeIcons.skipForward),
|
||||||
onPressed:
|
onPressed:
|
||||||
@ -200,13 +207,15 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
.watch(audioPlayerProvider.select((s) => s.loopMode));
|
.watch(audioPlayerProvider.select((s) => s.loopMode));
|
||||||
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
tooltip: Text(
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(
|
||||||
loopMode == PlaylistMode.single
|
loopMode == PlaylistMode.single
|
||||||
? context.l10n.loop_track
|
? context.l10n.loop_track
|
||||||
: loopMode == PlaylistMode.loop
|
: loopMode == PlaylistMode.loop
|
||||||
? context.l10n.repeat_playlist
|
? context.l10n.repeat_playlist
|
||||||
: "",
|
: "",
|
||||||
),
|
),
|
||||||
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
loopMode == PlaylistMode.single
|
loopMode == PlaylistMode.single
|
||||||
|
@ -160,7 +160,8 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.clear_all),
|
tooltip: TooltipContainer(
|
||||||
|
child: Text(context.l10n.clear_all)),
|
||||||
child: IconButton.outline(
|
child: IconButton.outline(
|
||||||
icon: const Icon(SpotubeIcons.playlistRemove),
|
icon: const Icon(SpotubeIcons.playlistRemove),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.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/context.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
@ -18,10 +19,18 @@ import 'package:spotube/utils/service_utils.dart';
|
|||||||
|
|
||||||
class PlaylistCard extends HookConsumerWidget {
|
class PlaylistCard extends HookConsumerWidget {
|
||||||
final PlaylistSimple playlist;
|
final PlaylistSimple playlist;
|
||||||
|
final bool _isTile;
|
||||||
|
|
||||||
const PlaylistCard(
|
const PlaylistCard(
|
||||||
this.playlist, {
|
this.playlist, {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
}) : _isTile = false;
|
||||||
|
|
||||||
|
const PlaylistCard.tile(
|
||||||
|
this.playlist, {
|
||||||
|
super.key,
|
||||||
|
}) : _isTile = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistQueue = ref.watch(audioPlayerProvider);
|
final playlistQueue = ref.watch(audioPlayerProvider);
|
||||||
@ -60,18 +69,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
|
return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlaybuttonCard(
|
void onTap() {
|
||||||
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: () {
|
|
||||||
ServiceUtils.pushNamed(
|
ServiceUtils.pushNamed(
|
||||||
context,
|
context,
|
||||||
PlaylistPage.name,
|
PlaylistPage.name,
|
||||||
@ -80,8 +78,9 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
extra: playlist,
|
extra: playlist,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
onPlaybuttonPressed: () async {
|
|
||||||
|
void onPlaybuttonPressed() async {
|
||||||
try {
|
try {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
if (isPlaylistPlaying && playing) {
|
if (isPlaylistPlaying && playing) {
|
||||||
@ -119,8 +118,9 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onAddToQueuePressed: () async {
|
|
||||||
|
void onAddToQueuePressed() async {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
try {
|
try {
|
||||||
if (isPlaylistPlaying) return;
|
if (isPlaylistPlaying) return;
|
||||||
@ -133,23 +133,64 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
playlistNotifier.addCollection(playlist.id!);
|
playlistNotifier.addCollection(playlist.id!);
|
||||||
historyNotifier.addPlaylists([playlist]);
|
historyNotifier.addPlaylists([playlist]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
showToast(
|
||||||
content: Text(context.l10n
|
context: context,
|
||||||
.added_num_tracks_to_queue(fetchedInitialTracks.length)),
|
builder: (context, overlay) {
|
||||||
action: SnackBarAction(
|
return SurfaceCard(
|
||||||
label: "Undo",
|
child: Basic(
|
||||||
|
content: Text(
|
||||||
|
context.l10n
|
||||||
|
.added_num_tracks_to_queue(fetchedInitialTracks.length),
|
||||||
|
),
|
||||||
|
trailing: Button.outline(
|
||||||
|
child: Text(context.l10n.undo),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier
|
playlistNotifier
|
||||||
.removeTracks(fetchedInitialTracks.map((e) => e.id!));
|
.removeTracks(fetchedInitialTracks.map((e) => e.id!));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(snackbar);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
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(
|
PlayerActions(
|
||||||
extraActions: [
|
extraActions: [
|
||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: Text(context.l10n.mini_player),
|
tooltip:
|
||||||
|
TooltipContainer(child: Text(context.l10n.mini_player)),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
variance: ButtonVariance.ghost,
|
variance: ButtonVariance.ghost,
|
||||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.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/components/playbutton_view/playbutton_view.dart';
|
||||||
import 'package:spotube/modules/album/album_card.dart';
|
import 'package:spotube/modules/album/album_card.dart';
|
||||||
import 'package:spotube/modules/artist/artist_card.dart';
|
import 'package:spotube/modules/artist/artist_card.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
|
||||||
import 'package:spotube/provider/spotify/views/home_section.dart';
|
import 'package:spotube/provider/spotify/views/home_section.dart';
|
||||||
|
|
||||||
class HomeFeedSectionPage extends HookConsumerWidget {
|
class HomeFeedSectionPage extends HookConsumerWidget {
|
||||||
@ -19,39 +20,63 @@ class HomeFeedSectionPage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final homeFeedSection = ref.watch(homeSectionViewProvider(sectionUri));
|
final homeFeedSection = ref.watch(homeSectionViewProvider(sectionUri));
|
||||||
final section = homeFeedSection.asData?.value ?? FakeData.feedSection;
|
final section = homeFeedSection.asData?.value ?? FakeData.feedSection;
|
||||||
|
final controller = useScrollController();
|
||||||
|
final isArtist = section.items.every((item) => item.artist != null);
|
||||||
|
|
||||||
return Skeletonizer(
|
return Skeletonizer(
|
||||||
enabled: homeFeedSection.isLoading,
|
enabled: homeFeedSection.isLoading,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: TitleBar(
|
headers: [
|
||||||
|
TitleBar(
|
||||||
title: Text(section.title ?? ""),
|
title: Text(section.title ?? ""),
|
||||||
automaticallyImplyLeading: true,
|
automaticallyImplyLeading: true,
|
||||||
),
|
)
|
||||||
body: CustomScrollView(
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: controller,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverLayoutBuilder(
|
if (isArtist)
|
||||||
builder: (context, constrains) {
|
SliverGrid.builder(
|
||||||
return SliverGrid.builder(
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 200,
|
maxCrossAxisExtent: 200,
|
||||||
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
mainAxisExtent: 250,
|
||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
),
|
),
|
||||||
itemCount: section.items.length,
|
itemCount: section.items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = section.items[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) {
|
if (item.album != null) {
|
||||||
return AlbumCard(item.album!.asAlbum);
|
return AlbumCard(item.album!.asAlbum);
|
||||||
} else if (item.artist != null) {
|
}
|
||||||
return ArtistCard(item.artist!.asArtist);
|
if (item.playlist != null) {
|
||||||
} else if (item.playlist != null) {
|
|
||||||
return PlaylistCard(item.playlist!.asPlaylist);
|
return PlaylistCard(item.playlist!.asPlaylist);
|
||||||
}
|
}
|
||||||
return const SizedBox();
|
return const SizedBox.shrink();
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SliverToBoxAdapter(
|
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:flutter_hooks/flutter_hooks.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:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart' hide Offset;
|
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/hooks/utils/use_custom_status_bar_color.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_card.dart';
|
import 'package:spotube/modules/playlist/playlist_card.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/components/waypoint.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class GenrePlaylistsPage extends HookConsumerWidget {
|
class GenrePlaylistsPage extends HookConsumerWidget {
|
||||||
@ -39,33 +40,37 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: kIsDesktop
|
headers: [
|
||||||
? const TitleBar(
|
if (kIsDesktop)
|
||||||
leading: [BackButton(color: Colors.white)],
|
const TitleBar(
|
||||||
|
leading: [
|
||||||
|
BackButton(),
|
||||||
|
],
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Colors.white,
|
surfaceOpacity: 0,
|
||||||
|
surfaceBlur: 0,
|
||||||
)
|
)
|
||||||
: null,
|
],
|
||||||
extendBodyBehindAppBar: true,
|
floatingHeader: true,
|
||||||
body: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: UniversalImage.imageProvider(category.icons!.first.url!),
|
image: UniversalImage.imageProvider(category.icons!.first.url!),
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
Colors.black.withOpacity(0.5),
|
|
||||||
BlendMode.darken,
|
|
||||||
),
|
|
||||||
repeat: ImageRepeat.noRepeat,
|
repeat: ImageRepeat.noRepeat,
|
||||||
matchTextDirection: true,
|
matchTextDirection: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: SurfaceCard(
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
automaticallyImplyLeading: kIsMobile,
|
automaticallyImplyLeading: false,
|
||||||
|
leading: kIsMobile ? const BackButton() : null,
|
||||||
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
|
||||||
title: const Text(""),
|
title: const Text(""),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@ -73,25 +78,25 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
|||||||
centerTitle: kIsDesktop,
|
centerTitle: kIsDesktop,
|
||||||
title: Text(
|
title: Text(
|
||||||
category.name!,
|
category.name!,
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: context.theme.typography.h3.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
letterSpacing: 3,
|
letterSpacing: 3,
|
||||||
shadows: [
|
shadows: [
|
||||||
const Shadow(
|
Shadow(
|
||||||
offset: Offset(-1.5, -1.5),
|
offset: const Offset(-1.5, -1.5),
|
||||||
color: Colors.black54,
|
color: Colors.black.withAlpha(138),
|
||||||
),
|
),
|
||||||
const Shadow(
|
Shadow(
|
||||||
offset: Offset(1.5, -1.5),
|
offset: const Offset(1.5, -1.5),
|
||||||
color: Colors.black54,
|
color: Colors.black.withAlpha(138),
|
||||||
),
|
),
|
||||||
const Shadow(
|
Shadow(
|
||||||
offset: Offset(1.5, 1.5),
|
offset: const Offset(1.5, 1.5),
|
||||||
color: Colors.black54,
|
color: Colors.black.withAlpha(138),
|
||||||
),
|
),
|
||||||
const Shadow(
|
Shadow(
|
||||||
offset: Offset(-1.5, 1.5),
|
offset: const Offset(-1.5, 1.5),
|
||||||
color: Colors.black54,
|
color: Colors.black.withAlpha(138),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -106,51 +111,16 @@ class GenrePlaylistsPage extends HookConsumerWidget {
|
|||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: mediaQuery.mdAndDown ? 12 : 24,
|
horizontal: mediaQuery.mdAndDown ? 12 : 24,
|
||||||
),
|
),
|
||||||
sliver: playlists.asData?.value.items.isNotEmpty != true
|
sliver: PlaybuttonView(
|
||||||
? 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(
|
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
isGrid: true,
|
itemCount: playlists.asData?.value.items.length ?? 0,
|
||||||
onTouchEdge: playlistsNotifier.fetchMore,
|
isLoading: playlists.isLoading,
|
||||||
child: PlaylistCard(FakeData.playlist),
|
hasMore: playlists.asData?.value.hasMore == true,
|
||||||
),
|
onRequestMore: playlistsNotifier.fetchMore,
|
||||||
);
|
listItemBuilder: (context, index) =>
|
||||||
}
|
PlaylistCard.tile(playlists.asData!.value.items[index]),
|
||||||
|
gridItemBuilder: (context, index) =>
|
||||||
return Skeleton.keep(
|
PlaylistCard(playlists.asData!.value.items[index]),
|
||||||
child: PlaylistCard(playlist),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -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