mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat(user-library): search for user playlists
This commit is contained in:
parent
e158dd0cec
commit
af4d56fd41
@ -11,7 +11,12 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
|||||||
|
|
||||||
class AlbumCard extends HookConsumerWidget {
|
class AlbumCard extends HookConsumerWidget {
|
||||||
final Album album;
|
final Album album;
|
||||||
const AlbumCard(this.album, {Key? key}) : super(key: key);
|
final PlaybuttonCardViewType viewType;
|
||||||
|
const AlbumCard(
|
||||||
|
this.album, {
|
||||||
|
Key? key,
|
||||||
|
this.viewType = PlaybuttonCardViewType.square,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
@ -25,6 +30,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
album.images,
|
album.images,
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
|
viewType: viewType,
|
||||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||||
isPlaying: isPlaylistPlaying && playback.isPlaying,
|
isPlaying: isPlaylistPlaying && playback.isPlaying,
|
||||||
isLoading: playback.status == PlaybackStatus.loading &&
|
isLoading: playback.status == PlaybackStatus.loading &&
|
||||||
|
@ -23,6 +23,7 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
||||||
|
|
||||||
const supportedAudioTypes = [
|
const supportedAudioTypes = [
|
||||||
"audio/webm",
|
"audio/webm",
|
||||||
@ -88,8 +89,15 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {"metadata": metadata, "file": f, "art": imageFile.path};
|
return {"metadata": metadata, "file": f, "art": imageFile.path};
|
||||||
} catch (e, stack) {
|
} on FfiException catch (e) {
|
||||||
getLogger(FutureProvider).e("[Fetching metadata]", e, stack);
|
if (e.message == "NoTag: reader does not contain an id3 tag") {
|
||||||
|
getLogger(FutureProvider<List<Track>>)
|
||||||
|
.w("[Fetching metadata]", e.message);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
} on Exception catch (e, stack) {
|
||||||
|
getLogger(FutureProvider<List<Track>>)
|
||||||
|
.e("[Fetching metadata]", e, stack);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,30 +1,44 @@
|
|||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||||
import 'package:flutter/material.dart' hide Image;
|
import 'package:flutter/material.dart' hide Image;
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:platform_ui/platform_ui.dart';
|
import 'package:platform_ui/platform_ui.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
||||||
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
||||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
||||||
import 'package:spotube/components/playlist/playlist_card.dart';
|
import 'package:spotube/components/playlist/playlist_card.dart';
|
||||||
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
||||||
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/provider/auth_provider.dart';
|
import 'package:spotube/provider/auth_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class UserPlaylists extends HookConsumerWidget {
|
class UserPlaylists extends HookConsumerWidget {
|
||||||
const UserPlaylists({Key? key}) : super(key: key);
|
const UserPlaylists({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
final searchText = useState('');
|
||||||
|
final breakpoint = useBreakpoints();
|
||||||
|
final spacing = useBreakpointValue<double>(
|
||||||
|
sm: 0,
|
||||||
|
others: 20,
|
||||||
|
);
|
||||||
|
final viewType = MediaQuery.of(context).size.width < 480
|
||||||
|
? PlaybuttonCardViewType.list
|
||||||
|
: PlaybuttonCardViewType.square;
|
||||||
final auth = ref.watch(authProvider);
|
final auth = ref.watch(authProvider);
|
||||||
if (auth.isAnonymous) {
|
|
||||||
return const AnonymousFallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
final playlistsQuery = useQuery(
|
final playlistsQuery = useQuery(
|
||||||
job: Queries.playlist.ofMine,
|
job: Queries.playlist.ofMine,
|
||||||
externalData: ref.watch(spotifyProvider),
|
externalData: ref.watch(spotifyProvider),
|
||||||
);
|
);
|
||||||
|
|
||||||
Image image = Image();
|
Image image = Image();
|
||||||
image.height = 300;
|
image.height = 300;
|
||||||
image.width = 300;
|
image.width = 300;
|
||||||
@ -37,27 +51,64 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
image.url = "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png";
|
image.url = "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png";
|
||||||
likedTracksPlaylist.images = [image];
|
likedTracksPlaylist.images = [image];
|
||||||
|
|
||||||
|
final playlists = useMemoized(
|
||||||
|
() => [
|
||||||
|
likedTracksPlaylist,
|
||||||
|
...?playlistsQuery.data,
|
||||||
|
]
|
||||||
|
.map((e) => Tuple2(
|
||||||
|
searchText.value.isEmpty
|
||||||
|
? 100
|
||||||
|
: weightedRatio(e.name!, searchText.value),
|
||||||
|
e,
|
||||||
|
))
|
||||||
|
.sorted((a, b) => b.item1.compareTo(a.item1))
|
||||||
|
.where((e) => e.item1 > 50)
|
||||||
|
.map((e) => e.item2)
|
||||||
|
.toList(),
|
||||||
|
[playlistsQuery.data, searchText.value],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (auth.isAnonymous) {
|
||||||
|
return const AnonymousFallback();
|
||||||
|
}
|
||||||
if (playlistsQuery.isLoading || !playlistsQuery.hasData) {
|
if (playlistsQuery.isLoading || !playlistsQuery.hasData) {
|
||||||
return const Center(child: ShimmerPlaybuttonCard(count: 7));
|
return const Center(child: ShimmerPlaybuttonCard(count: 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final children = [
|
||||||
|
const PlaylistCreateDialog(),
|
||||||
|
...playlists
|
||||||
|
.map((playlist) => PlaylistCard(
|
||||||
|
playlist,
|
||||||
|
viewType: viewType,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
];
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||||
child: Container(
|
child: Padding(
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Wrap(
|
child: Column(
|
||||||
spacing: 20, // gap between adjacent chips
|
|
||||||
runSpacing: 20, // gap between lines
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
const PlaylistCreateDialog(),
|
PlatformTextField(
|
||||||
PlaylistCard(likedTracksPlaylist),
|
onChanged: (value) => searchText.value = value,
|
||||||
...playlistsQuery.data!
|
placeholder: "Search your playlists...",
|
||||||
.map((playlist) => PlaylistCard(playlist))
|
prefixIcon: Icons.search,
|
||||||
.toList(),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Center(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: spacing, // gap between adjacent chips
|
||||||
|
runSpacing: 20, // gap between lines
|
||||||
|
alignment: breakpoint.isSm
|
||||||
|
? WrapAlignment.center
|
||||||
|
: WrapAlignment.start,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -11,7 +11,12 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
|||||||
|
|
||||||
class PlaylistCard extends HookConsumerWidget {
|
class PlaylistCard extends HookConsumerWidget {
|
||||||
final PlaylistSimple playlist;
|
final PlaylistSimple playlist;
|
||||||
const PlaylistCard(this.playlist, {Key? key}) : super(key: key);
|
final PlaybuttonCardViewType viewType;
|
||||||
|
const PlaylistCard(
|
||||||
|
this.playlist, {
|
||||||
|
Key? key,
|
||||||
|
this.viewType = PlaybuttonCardViewType.square,
|
||||||
|
}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
@ -21,6 +26,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
final int marginH =
|
final int marginH =
|
||||||
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
|
viewType: viewType,
|
||||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||||
title: playlist.name!,
|
title: playlist.name!,
|
||||||
imageUrl: TypeConversionUtils.image_X_UrlString(
|
imageUrl: TypeConversionUtils.image_X_UrlString(
|
||||||
|
@ -15,7 +15,9 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
|
|
||||||
return PlatformTextButton(
|
return SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: PlatformTextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showPlatformAlertDialog(
|
showPlatformAlertDialog(
|
||||||
context,
|
context,
|
||||||
@ -46,7 +48,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
return PlatformAlertDialog(
|
return PlatformAlertDialog(
|
||||||
macosAppIcon: Sidebar.brandLogo(),
|
macosAppIcon: Sidebar.brandLogo(),
|
||||||
title: const Text("Create a Playlist"),
|
title: const PlatformText("Create a Playlist"),
|
||||||
primaryActions: [
|
primaryActions: [
|
||||||
PlatformBuilder(
|
PlatformBuilder(
|
||||||
fallback: PlatformBuilderFallback.android,
|
fallback: PlatformBuilderFallback.android,
|
||||||
@ -116,7 +118,8 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
PlatformCheckbox(
|
PlatformCheckbox(
|
||||||
value: collaborative.value,
|
value: collaborative.value,
|
||||||
label: const PlatformText("Collaborative"),
|
label: const PlatformText("Collaborative"),
|
||||||
onChanged: (val) => collaborative.value = val ?? false,
|
onChanged: (val) =>
|
||||||
|
collaborative.value = val ?? false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -128,16 +131,18 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: MaterialStateProperty.all(
|
padding: MaterialStateProperty.all(
|
||||||
const EdgeInsets.symmetric(horizontal: 15, vertical: 100)),
|
const EdgeInsets.symmetric(vertical: 100),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: const [
|
children: const [
|
||||||
Icon(Icons.add_box_rounded, size: 50),
|
Icon(Icons.add_box_rounded, size: 40),
|
||||||
Text("Create Playlist", style: TextStyle(fontSize: 22)),
|
PlatformText("Create Playlist", style: TextStyle(fontSize: 20)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ import 'package:spotube/components/shared/spotube_marquee_text.dart';
|
|||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/hooks/use_platform_property.dart';
|
import 'package:spotube/hooks/use_platform_property.dart';
|
||||||
|
|
||||||
|
enum PlaybuttonCardViewType { square, list }
|
||||||
|
|
||||||
class PlaybuttonCard extends HookWidget {
|
class PlaybuttonCard extends HookWidget {
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
final void Function()? onPlaybuttonPressed;
|
final void Function()? onPlaybuttonPressed;
|
||||||
@ -15,6 +17,8 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
final bool isPlaying;
|
final bool isPlaying;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String title;
|
final String title;
|
||||||
|
final PlaybuttonCardViewType viewType;
|
||||||
|
|
||||||
const PlaybuttonCard({
|
const PlaybuttonCard({
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
required this.isPlaying,
|
required this.isPlaying,
|
||||||
@ -24,6 +28,7 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
this.description,
|
this.description,
|
||||||
this.onPlaybuttonPressed,
|
this.onPlaybuttonPressed,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.viewType = PlaybuttonCardViewType.square,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -56,7 +61,7 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final iconBgColor = PlatformTheme.of(context).primaryColor;
|
final isSquare = viewType == PlaybuttonCardViewType.square;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: margin,
|
margin: margin,
|
||||||
@ -66,8 +71,132 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
splashFactory: splash,
|
splashFactory: splash,
|
||||||
highlightColor: Colors.black12,
|
highlightColor: Colors.black12,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 200),
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: isSquare ? 200 : double.infinity,
|
||||||
|
maxHeight: !isSquare ? 60 : double.infinity,
|
||||||
|
),
|
||||||
child: HoverBuilder(builder: (context, isHovering) {
|
child: HoverBuilder(builder: (context, isHovering) {
|
||||||
|
final playButton = PlatformIconButton(
|
||||||
|
onPressed: onPlaybuttonPressed,
|
||||||
|
backgroundColor: PlatformTheme.of(context).primaryColor,
|
||||||
|
hoverColor:
|
||||||
|
PlatformTheme.of(context).primaryColor?.withOpacity(0.5),
|
||||||
|
icon: isLoading
|
||||||
|
? SizedBox(
|
||||||
|
height: 23,
|
||||||
|
width: 23,
|
||||||
|
child: PlatformCircularProgressIndicator(
|
||||||
|
color: ThemeData.estimateBrightnessForColor(
|
||||||
|
PlatformTheme.of(context).primaryColor!,
|
||||||
|
) ==
|
||||||
|
Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.grey[900],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
isPlaying
|
||||||
|
? Icons.pause_rounded
|
||||||
|
: Icons.play_arrow_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final image = Padding(
|
||||||
|
padding: EdgeInsets.all(
|
||||||
|
platform == TargetPlatform.windows ? 5 : 0,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
[TargetPlatform.windows, TargetPlatform.linux]
|
||||||
|
.contains(platform)
|
||||||
|
? 5
|
||||||
|
: 8,
|
||||||
|
),
|
||||||
|
child: UniversalImage(
|
||||||
|
path: imageUrl,
|
||||||
|
width: isSquare ? 200 : 60,
|
||||||
|
placeholder: (context, url) =>
|
||||||
|
Image.asset("assets/placeholder.png"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final square = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// thumbnail of the playlist
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
image,
|
||||||
|
Positioned.directional(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
bottom: 10,
|
||||||
|
end: 5,
|
||||||
|
child: playButton,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Tooltip(
|
||||||
|
message: title,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 20,
|
||||||
|
child: SpotubeMarqueeText(
|
||||||
|
text: title,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
isHovering: isHovering,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (description != null) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: SpotubeMarqueeText(
|
||||||
|
text: description!,
|
||||||
|
style: PlatformTextTheme.of(context).caption,
|
||||||
|
isHovering: isHovering,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final list = Row(
|
||||||
|
children: [
|
||||||
|
// thumbnail of the playlist
|
||||||
|
image,
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
PlatformText(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
if (description != null)
|
||||||
|
PlatformText(
|
||||||
|
description!,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: PlatformTextTheme.of(context).caption,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
playButton,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return Ink(
|
return Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
@ -89,103 +218,7 @@ class PlaybuttonCard extends HookWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: isSquare ? square : list,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// thumbnail of the playlist
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(
|
|
||||||
platform == TargetPlatform.windows ? 5 : 0,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
[TargetPlatform.windows, TargetPlatform.linux]
|
|
||||||
.contains(platform)
|
|
||||||
? 5
|
|
||||||
: 8,
|
|
||||||
),
|
|
||||||
child: UniversalImage(
|
|
||||||
path: imageUrl,
|
|
||||||
width: 200,
|
|
||||||
placeholder: (context, url) =>
|
|
||||||
Image.asset("assets/placeholder.png"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.directional(
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
bottom: 10,
|
|
||||||
end: 5,
|
|
||||||
child: Builder(builder: (context) {
|
|
||||||
return PlatformIconButton(
|
|
||||||
onPressed: onPlaybuttonPressed,
|
|
||||||
backgroundColor:
|
|
||||||
PlatformTheme.of(context).primaryColor,
|
|
||||||
hoverColor: PlatformTheme.of(context)
|
|
||||||
.primaryColor
|
|
||||||
?.withOpacity(0.5),
|
|
||||||
icon: isLoading
|
|
||||||
? SizedBox(
|
|
||||||
height: 23,
|
|
||||||
width: 23,
|
|
||||||
child: PlatformCircularProgressIndicator(
|
|
||||||
color:
|
|
||||||
ThemeData.estimateBrightnessForColor(
|
|
||||||
PlatformTheme.of(context)
|
|
||||||
.primaryColor!,
|
|
||||||
) ==
|
|
||||||
Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: Colors.grey[900],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
isPlaying
|
|
||||||
? Icons.pause_rounded
|
|
||||||
: Icons.play_arrow_rounded,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 5),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16, vertical: 10),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Tooltip(
|
|
||||||
message: title,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 20,
|
|
||||||
child: SpotubeMarqueeText(
|
|
||||||
text: title,
|
|
||||||
style:
|
|
||||||
const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
isHovering: isHovering,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (description != null) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
SizedBox(
|
|
||||||
height: 30,
|
|
||||||
child: SpotubeMarqueeText(
|
|
||||||
text: description!,
|
|
||||||
style: PlatformTextTheme.of(context).caption,
|
|
||||||
isHovering: isHovering,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -30,7 +30,8 @@ class Waypoint extends HookWidget {
|
|||||||
// nextPageTrigger will have a value equivalent to 80% of the list size.
|
// nextPageTrigger will have a value equivalent to 80% of the list size.
|
||||||
final nextPageTrigger = 0.8 * controller.position.maxScrollExtent;
|
final nextPageTrigger = 0.8 * controller.position.maxScrollExtent;
|
||||||
|
|
||||||
// scrollController fetches the next paginated data when the current postion of the user on the screen has surpassed
|
// scrollController fetches the next paginated data when the current
|
||||||
|
// position of the user on the screen has surpassed
|
||||||
if (controller.position.pixels >= nextPageTrigger && isMounted()) {
|
if (controller.position.pixels >= nextPageTrigger && isMounted()) {
|
||||||
await onTouchEdge?.call();
|
await onTouchEdge?.call();
|
||||||
}
|
}
|
||||||
@ -39,9 +40,8 @@ class Waypoint extends HookWidget {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (controller.hasClients && isMounted()) {
|
if (controller.hasClients && isMounted()) {
|
||||||
listener();
|
listener();
|
||||||
}
|
|
||||||
|
|
||||||
controller.addListener(listener);
|
controller.addListener(listener);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return () => controller.removeListener(listener);
|
return () => controller.removeListener(listener);
|
||||||
}, [controller, onTouchEdge, isMounted]);
|
}, [controller, onTouchEdge, isMounted]);
|
||||||
|
@ -57,7 +57,12 @@ class _AutoScrollControllerHook extends Hook<AutoScrollController> {
|
|||||||
|
|
||||||
class _AutoScrollControllerHookState
|
class _AutoScrollControllerHookState
|
||||||
extends HookState<AutoScrollController, _AutoScrollControllerHook> {
|
extends HookState<AutoScrollController, _AutoScrollControllerHook> {
|
||||||
late final controller = AutoScrollController(
|
late final AutoScrollController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initHook() {
|
||||||
|
super.initHook();
|
||||||
|
controller = AutoScrollController(
|
||||||
initialScrollOffset: hook.initialScrollOffset,
|
initialScrollOffset: hook.initialScrollOffset,
|
||||||
keepScrollOffset: hook.keepScrollOffset,
|
keepScrollOffset: hook.keepScrollOffset,
|
||||||
debugLabel: hook.debugLabel,
|
debugLabel: hook.debugLabel,
|
||||||
@ -66,6 +71,7 @@ class _AutoScrollControllerHookState
|
|||||||
suggestedRowHeight: hook.suggestedRowHeight,
|
suggestedRowHeight: hook.suggestedRowHeight,
|
||||||
viewportBoundaryGetter: hook.viewportBoundaryGetter,
|
viewportBoundaryGetter: hook.viewportBoundaryGetter,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AutoScrollController build(BuildContext context) => controller;
|
AutoScrollController build(BuildContext context) => controller;
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
|
|
||||||
useBreakpointValue({sm, md, lg, xl, xxl}) {
|
useBreakpointValue<T>({
|
||||||
|
T? sm,
|
||||||
|
T? md,
|
||||||
|
T? lg,
|
||||||
|
T? xl,
|
||||||
|
T? xxl,
|
||||||
|
T? others,
|
||||||
|
}) {
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
if (breakpoint.isSm) {
|
if (breakpoint.isSm) {
|
||||||
return sm;
|
return sm ?? others;
|
||||||
} else if (breakpoint.isMd) {
|
} else if (breakpoint.isMd) {
|
||||||
return md;
|
return md ?? others;
|
||||||
} else if (breakpoint.isXl) {
|
} else if (breakpoint.isXl) {
|
||||||
return xl;
|
return xl ?? others;
|
||||||
} else if (breakpoint.isXxl) {
|
} else if (breakpoint.isXxl) {
|
||||||
return xxl;
|
return xxl ?? others;
|
||||||
} else {
|
} else {
|
||||||
return lg;
|
return lg ?? others;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -646,6 +646,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
|
fuzzywuzzy:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fuzzywuzzy
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -68,6 +68,7 @@ dependencies:
|
|||||||
libadwaita: ^1.2.5
|
libadwaita: ^1.2.5
|
||||||
adwaita: ^0.5.2
|
adwaita: ^0.5.2
|
||||||
flutter_svg: ^1.1.6
|
flutter_svg: ^1.1.6
|
||||||
|
fuzzywuzzy: ^0.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user