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 {
|
||||
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
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -25,6 +30,7 @@ class AlbumCard extends HookConsumerWidget {
|
||||
album.images,
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
),
|
||||
viewType: viewType,
|
||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||
isPlaying: isPlaylistPlaying && playback.isPlaying,
|
||||
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/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
||||
|
||||
const supportedAudioTypes = [
|
||||
"audio/webm",
|
||||
@ -88,8 +89,15 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
||||
}
|
||||
|
||||
return {"metadata": metadata, "file": f, "art": imageFile.path};
|
||||
} catch (e, stack) {
|
||||
getLogger(FutureProvider).e("[Fetching metadata]", e, stack);
|
||||
} on FfiException catch (e) {
|
||||
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 {};
|
||||
}
|
||||
},
|
||||
|
@ -1,30 +1,44 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
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:platform_ui/platform_ui.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/fallbacks/anonymous_fallback.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/spotify_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class UserPlaylists extends HookConsumerWidget {
|
||||
const UserPlaylists({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
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);
|
||||
if (auth.isAnonymous) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
|
||||
final playlistsQuery = useQuery(
|
||||
job: Queries.playlist.ofMine,
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
);
|
||||
|
||||
Image image = Image();
|
||||
image.height = 300;
|
||||
image.width = 300;
|
||||
@ -37,27 +51,64 @@ class UserPlaylists extends HookConsumerWidget {
|
||||
image.url = "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png";
|
||||
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) {
|
||||
return const Center(child: ShimmerPlaybuttonCard(count: 7));
|
||||
}
|
||||
|
||||
final children = [
|
||||
const PlaylistCreateDialog(),
|
||||
...playlists
|
||||
.map((playlist) => PlaylistCard(
|
||||
playlist,
|
||||
viewType: viewType,
|
||||
))
|
||||
.toList(),
|
||||
];
|
||||
return SingleChildScrollView(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Wrap(
|
||||
spacing: 20, // gap between adjacent chips
|
||||
runSpacing: 20, // gap between lines
|
||||
alignment: WrapAlignment.center,
|
||||
child: Column(
|
||||
children: [
|
||||
const PlaylistCreateDialog(),
|
||||
PlaylistCard(likedTracksPlaylist),
|
||||
...playlistsQuery.data!
|
||||
.map((playlist) => PlaylistCard(playlist))
|
||||
.toList(),
|
||||
PlatformTextField(
|
||||
onChanged: (value) => searchText.value = value,
|
||||
placeholder: "Search your playlists...",
|
||||
prefixIcon: Icons.search,
|
||||
),
|
||||
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 {
|
||||
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
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
@ -21,6 +26,7 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
final int marginH =
|
||||
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
||||
return PlaybuttonCard(
|
||||
viewType: viewType,
|
||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||
title: playlist.name!,
|
||||
imageUrl: TypeConversionUtils.image_X_UrlString(
|
||||
|
@ -15,128 +15,133 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
|
||||
return PlatformTextButton(
|
||||
onPressed: () {
|
||||
showPlatformAlertDialog(
|
||||
context,
|
||||
builder: (context) {
|
||||
return HookBuilder(builder: (context) {
|
||||
final playlistName = useTextEditingController();
|
||||
final description = useTextEditingController();
|
||||
final public = useState(false);
|
||||
final collaborative = useState(false);
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
child: PlatformTextButton(
|
||||
onPressed: () {
|
||||
showPlatformAlertDialog(
|
||||
context,
|
||||
builder: (context) {
|
||||
return HookBuilder(builder: (context) {
|
||||
final playlistName = useTextEditingController();
|
||||
final description = useTextEditingController();
|
||||
final public = useState(false);
|
||||
final collaborative = useState(false);
|
||||
|
||||
onCreate() async {
|
||||
if (playlistName.text.isEmpty) return;
|
||||
final me = await spotify.me.get();
|
||||
await spotify.playlists.createPlaylist(
|
||||
me.id!,
|
||||
playlistName.text,
|
||||
collaborative: collaborative.value,
|
||||
public: public.value,
|
||||
description: description.text,
|
||||
onCreate() async {
|
||||
if (playlistName.text.isEmpty) return;
|
||||
final me = await spotify.me.get();
|
||||
await spotify.playlists.createPlaylist(
|
||||
me.id!,
|
||||
playlistName.text,
|
||||
collaborative: collaborative.value,
|
||||
public: public.value,
|
||||
description: description.text,
|
||||
);
|
||||
await QueryBowl.of(context)
|
||||
.getQuery(
|
||||
Queries.playlist.ofMine.queryKey,
|
||||
)
|
||||
?.refetch();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: const PlatformText("Create a Playlist"),
|
||||
primaryActions: [
|
||||
PlatformBuilder(
|
||||
fallback: PlatformBuilderFallback.android,
|
||||
android: (context, _) {
|
||||
return PlatformFilledButton(
|
||||
onPressed: onCreate,
|
||||
child: const Text("Create"),
|
||||
);
|
||||
},
|
||||
ios: (context, data) {
|
||||
return CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: onCreate,
|
||||
child: const Text("Create"),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
secondaryActions: [
|
||||
PlatformBuilder(
|
||||
fallback: PlatformBuilderFallback.android,
|
||||
android: (context, _) {
|
||||
return PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
ios: (context, data) {
|
||||
return CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
isDestructiveAction: true,
|
||||
child: const Text("Cancel"),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
content: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
PlatformTextField(
|
||||
controller: playlistName,
|
||||
placeholder: "Name of the playlist",
|
||||
label: "Playlist Name",
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
PlatformTextField(
|
||||
controller: description,
|
||||
placeholder: "Description...",
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
PlatformCheckbox(
|
||||
value: public.value,
|
||||
label: const PlatformText("Public"),
|
||||
onChanged: (val) => public.value = val ?? false,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
PlatformCheckbox(
|
||||
value: collaborative.value,
|
||||
label: const PlatformText("Collaborative"),
|
||||
onChanged: (val) =>
|
||||
collaborative.value = val ?? false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
await QueryBowl.of(context)
|
||||
.getQuery(
|
||||
Queries.playlist.ofMine.queryKey,
|
||||
)
|
||||
?.refetch();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: const Text("Create a Playlist"),
|
||||
primaryActions: [
|
||||
PlatformBuilder(
|
||||
fallback: PlatformBuilderFallback.android,
|
||||
android: (context, _) {
|
||||
return PlatformFilledButton(
|
||||
onPressed: onCreate,
|
||||
child: const Text("Create"),
|
||||
);
|
||||
},
|
||||
ios: (context, data) {
|
||||
return CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: onCreate,
|
||||
child: const Text("Create"),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
secondaryActions: [
|
||||
PlatformBuilder(
|
||||
fallback: PlatformBuilderFallback.android,
|
||||
android: (context, _) {
|
||||
return PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
ios: (context, data) {
|
||||
return CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
isDestructiveAction: true,
|
||||
child: const Text("Cancel"),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
content: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
PlatformTextField(
|
||||
controller: playlistName,
|
||||
placeholder: "Name of the playlist",
|
||||
label: "Playlist Name",
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
PlatformTextField(
|
||||
controller: description,
|
||||
placeholder: "Description...",
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
PlatformCheckbox(
|
||||
value: public.value,
|
||||
label: const PlatformText("Public"),
|
||||
onChanged: (val) => public.value = val ?? false,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
PlatformCheckbox(
|
||||
value: collaborative.value,
|
||||
label: const PlatformText("Collaborative"),
|
||||
onChanged: (val) => collaborative.value = val ?? false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 15, vertical: 100)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.add_box_rounded, size: 50),
|
||||
Text("Create Playlist", style: TextStyle(fontSize: 22)),
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(vertical: 100),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.add_box_rounded, size: 40),
|
||||
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/hooks/use_platform_property.dart';
|
||||
|
||||
enum PlaybuttonCardViewType { square, list }
|
||||
|
||||
class PlaybuttonCard extends HookWidget {
|
||||
final void Function()? onTap;
|
||||
final void Function()? onPlaybuttonPressed;
|
||||
@ -15,6 +17,8 @@ class PlaybuttonCard extends HookWidget {
|
||||
final bool isPlaying;
|
||||
final bool isLoading;
|
||||
final String title;
|
||||
final PlaybuttonCardViewType viewType;
|
||||
|
||||
const PlaybuttonCard({
|
||||
required this.imageUrl,
|
||||
required this.isPlaying,
|
||||
@ -24,6 +28,7 @@ class PlaybuttonCard extends HookWidget {
|
||||
this.description,
|
||||
this.onPlaybuttonPressed,
|
||||
this.onTap,
|
||||
this.viewType = PlaybuttonCardViewType.square,
|
||||
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(
|
||||
margin: margin,
|
||||
@ -66,8 +71,132 @@ class PlaybuttonCard extends HookWidget {
|
||||
splashFactory: splash,
|
||||
highlightColor: Colors.black12,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: isSquare ? 200 : double.infinity,
|
||||
maxHeight: !isSquare ? 60 : double.infinity,
|
||||
),
|
||||
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(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
@ -89,103 +218,7 @@ class PlaybuttonCard extends HookWidget {
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: isSquare ? square : list,
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -30,7 +30,8 @@ class Waypoint extends HookWidget {
|
||||
// nextPageTrigger will have a value equivalent to 80% of the list size.
|
||||
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()) {
|
||||
await onTouchEdge?.call();
|
||||
}
|
||||
@ -39,9 +40,8 @@ class Waypoint extends HookWidget {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (controller.hasClients && isMounted()) {
|
||||
listener();
|
||||
controller.addListener(listener);
|
||||
}
|
||||
|
||||
controller.addListener(listener);
|
||||
});
|
||||
return () => controller.removeListener(listener);
|
||||
}, [controller, onTouchEdge, isMounted]);
|
||||
|
@ -57,15 +57,21 @@ class _AutoScrollControllerHook extends Hook<AutoScrollController> {
|
||||
|
||||
class _AutoScrollControllerHookState
|
||||
extends HookState<AutoScrollController, _AutoScrollControllerHook> {
|
||||
late final controller = AutoScrollController(
|
||||
initialScrollOffset: hook.initialScrollOffset,
|
||||
keepScrollOffset: hook.keepScrollOffset,
|
||||
debugLabel: hook.debugLabel,
|
||||
axis: hook.axis,
|
||||
copyTagsFrom: hook.copyTagsFrom,
|
||||
suggestedRowHeight: hook.suggestedRowHeight,
|
||||
viewportBoundaryGetter: hook.viewportBoundaryGetter,
|
||||
);
|
||||
late final AutoScrollController controller;
|
||||
|
||||
@override
|
||||
void initHook() {
|
||||
super.initHook();
|
||||
controller = AutoScrollController(
|
||||
initialScrollOffset: hook.initialScrollOffset,
|
||||
keepScrollOffset: hook.keepScrollOffset,
|
||||
debugLabel: hook.debugLabel,
|
||||
axis: hook.axis,
|
||||
copyTagsFrom: hook.copyTagsFrom,
|
||||
suggestedRowHeight: hook.suggestedRowHeight,
|
||||
viewportBoundaryGetter: hook.viewportBoundaryGetter,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoScrollController build(BuildContext context) => controller;
|
||||
|
@ -1,17 +1,24 @@
|
||||
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();
|
||||
|
||||
if (breakpoint.isSm) {
|
||||
return sm;
|
||||
return sm ?? others;
|
||||
} else if (breakpoint.isMd) {
|
||||
return md;
|
||||
return md ?? others;
|
||||
} else if (breakpoint.isXl) {
|
||||
return xl;
|
||||
return xl ?? others;
|
||||
} else if (breakpoint.isXxl) {
|
||||
return xxl;
|
||||
return xxl ?? others;
|
||||
} else {
|
||||
return lg;
|
||||
return lg ?? others;
|
||||
}
|
||||
}
|
||||
|
@ -646,6 +646,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
fuzzywuzzy:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fuzzywuzzy
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -68,6 +68,7 @@ dependencies:
|
||||
libadwaita: ^1.2.5
|
||||
adwaita: ^0.5.2
|
||||
flutter_svg: ^1.1.6
|
||||
fuzzywuzzy: ^0.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user