mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
PlaylistView shows image & extra data as header
Loaders & error placeholders added PlayerOverlay swipe to open PlayerView fix: Logout functionality broken
This commit is contained in:
parent
063b239b5d
commit
71d6fc5a4a
BIN
assets/empty_box.png
Normal file
BIN
assets/empty_box.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
@ -41,7 +41,6 @@ class AlbumView extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
|
||||
final isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
|
||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
|
||||
@ -49,18 +48,35 @@ class AlbumView extends HookConsumerWidget {
|
||||
final albumSavedSnapshot =
|
||||
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
PageWindowTitleBar(
|
||||
leading: Row(
|
||||
children: [
|
||||
// nav back
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
if (auth.isLoggedIn)
|
||||
albumSavedSnapshot.when(
|
||||
final albumArt =
|
||||
useMemoized(() => imageToUrlString(album.images), [album.images]);
|
||||
|
||||
return TrackCollectionView(
|
||||
id: album.id!,
|
||||
isPlaying: playback.currentPlaylist?.id != null &&
|
||||
playback.currentPlaylist?.id == album.id,
|
||||
title: album.name!,
|
||||
titleImage: albumArt,
|
||||
tracksSnapshot: tracksSnapshot,
|
||||
album: album,
|
||||
onPlay: ([track]) {
|
||||
if (tracksSnapshot.asData?.value != null) {
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value
|
||||
.map((track) => simpleTrackToTrack(track, album))
|
||||
.toList(),
|
||||
currentTrack: track,
|
||||
);
|
||||
}
|
||||
},
|
||||
onShare: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: "https://open.spotify.com/album/${album.id}"),
|
||||
);
|
||||
},
|
||||
heartBtn: auth.isLoggedIn
|
||||
? albumSavedSnapshot.when(
|
||||
data: (isSaved) {
|
||||
return HeartButton(
|
||||
isLiked: isSaved,
|
||||
@ -84,50 +100,8 @@ class AlbumView extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const CircularProgressIndicator()),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: tracksSnapshot.asData?.value != null
|
||||
? () => playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value.map((trackSmp) {
|
||||
return simpleTrackToTrack(trackSmp, album);
|
||||
}).toList(),
|
||||
)
|
||||
loading: () => const CircularProgressIndicator())
|
||||
: null,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(album.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
tracksSnapshot.when(
|
||||
data: (data) {
|
||||
List<Track> tracks = data.map((trackSmp) {
|
||||
return simpleTrackToTrack(trackSmp, album);
|
||||
}).toList();
|
||||
return TracksTableView(
|
||||
tracks,
|
||||
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
||||
playback,
|
||||
tracks,
|
||||
currentTrack: currentTrack,
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const ShimmerTrackTile(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/components/Shared/NotFound.dart';
|
||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
@ -72,6 +73,9 @@ class CategoryCard extends HookConsumerWidget {
|
||||
scrollController: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
|
||||
noItemsFoundIndicatorBuilder: (context) {
|
||||
return const NotFound();
|
||||
},
|
||||
firstPageProgressIndicatorBuilder: (context) {
|
||||
return const ShimmerPlaybuttonCard();
|
||||
},
|
||||
|
@ -3,11 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart' hide Image;
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/models/sideBarTiles.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class Sidebar extends HookConsumerWidget {
|
||||
final int selectedIndex;
|
||||
@ -36,7 +35,7 @@ class Sidebar extends HookConsumerWidget {
|
||||
final breakpoints = useBreakpoints();
|
||||
if (breakpoints.isSm) return Container();
|
||||
final extended = useState(false);
|
||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final meSnapshot = ref.watch(currentUserQuery);
|
||||
|
||||
useEffect(() {
|
||||
if (breakpoints.isMd && extended.value) {
|
||||
@ -78,11 +77,10 @@ class Sidebar extends HookConsumerWidget {
|
||||
]),
|
||||
)
|
||||
: _buildSmallLogo(),
|
||||
trailing: FutureBuilder<User>(
|
||||
future: spotify.me.get(),
|
||||
builder: (context, snapshot) {
|
||||
final avatarImg = imageToUrlString(snapshot.data?.images,
|
||||
index: (snapshot.data?.images?.length ?? 1) - 1);
|
||||
trailing: meSnapshot.when(
|
||||
data: (data) {
|
||||
final avatarImg = imageToUrlString(data.images,
|
||||
index: (data.images?.length ?? 1) - 1);
|
||||
return extended.value
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -97,7 +95,7 @@ class Sidebar extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
snapshot.data?.displayName ?? "Guest",
|
||||
data.displayName ?? "Guest",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@ -116,6 +114,8 @@ class Sidebar extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (e, _) => Text("Error $e"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/helpers/simple-album-to-album.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
@ -25,7 +26,7 @@ class UserAlbums extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator.adaptive()),
|
||||
loading: () => const Center(child: ShimmerPlaybuttonCard(count: 7)),
|
||||
error: (_, __) => const Text("Failure is the pillar of success"),
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
@ -13,8 +14,7 @@ class UserPlaylists extends ConsumerWidget {
|
||||
final playlists = ref.watch(currentUserPlaylistsQuery);
|
||||
|
||||
return playlists.when(
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
loading: () => const Center(child: ShimmerPlaybuttonCard(count: 7)),
|
||||
data: (data) {
|
||||
Image image = Image();
|
||||
image.height = 300;
|
||||
|
@ -37,6 +37,14 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
right: (breakpoint.isMd ? 10 : 5),
|
||||
left: (breakpoint.isSm ? 5 : 80),
|
||||
bottom: (breakpoint.isSm ? 63 : 10),
|
||||
child: GestureDetector(
|
||||
onVerticalDragEnd: (details) {
|
||||
int sensitivity = 8;
|
||||
if (details.primaryVelocity != null &&
|
||||
details.primaryVelocity! < -sensitivity) {
|
||||
GoRouter.of(context).push("/player");
|
||||
}
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
@ -90,6 +98,7 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
child: Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
leading: BackButton(),
|
||||
transparent: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
backgroundColor: paletteColor.color,
|
||||
body: Column(
|
||||
|
@ -1,12 +1,12 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
@ -53,27 +53,62 @@ class PlaylistView extends HookConsumerWidget {
|
||||
final meSnapshot = ref.watch(currentUserQuery);
|
||||
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
PageWindowTitleBar(
|
||||
leading: Row(
|
||||
children: [
|
||||
// nav back
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
if (auth.isLoggedIn && playlist.id != "user-liked-tracks")
|
||||
meSnapshot.when(
|
||||
final titleImage =
|
||||
useMemoized(() => imageToUrlString(playlist.images), [playlist.images]);
|
||||
|
||||
final color = usePaletteGenerator(
|
||||
context,
|
||||
titleImage,
|
||||
).dominantColor;
|
||||
|
||||
return TrackCollectionView(
|
||||
id: playlist.id!,
|
||||
isPlaying: isPlaylistPlaying,
|
||||
title: playlist.name!,
|
||||
titleImage: titleImage,
|
||||
tracksSnapshot: tracksSnapshot,
|
||||
description: playlist.description,
|
||||
isOwned: playlist.owner?.id != null &&
|
||||
playlist.owner!.id == meSnapshot.asData?.value.id,
|
||||
onPlay: ([track]) {
|
||||
if (tracksSnapshot.asData?.value != null) {
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value,
|
||||
currentTrack: track,
|
||||
);
|
||||
}
|
||||
},
|
||||
showShare: playlist.id != "user-liked-tracks",
|
||||
onShare: () {
|
||||
final data = "https://open.spotify.com/playlist/${playlist.id}";
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: data),
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
"Copied $data to clipboard",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
heartBtn: (auth.isLoggedIn && playlist.id != "user-liked-tracks"
|
||||
? meSnapshot.when(
|
||||
data: (me) {
|
||||
final query = playlistIsFollowedQuery(jsonEncode(
|
||||
{"playlistId": playlist.id, "userId": me.id!}));
|
||||
final query = playlistIsFollowedQuery(
|
||||
jsonEncode({"playlistId": playlist.id, "userId": me.id!}));
|
||||
final followingSnapshot = ref.watch(query);
|
||||
|
||||
return followingSnapshot.when(
|
||||
data: (isFollowing) {
|
||||
return HeartButton(
|
||||
isLiked: isFollowing,
|
||||
color: color?.titleTextColor,
|
||||
icon: playlist.owner?.id != null &&
|
||||
me.id == playlist.owner?.id
|
||||
? Icons.delete_outline_rounded
|
||||
@ -81,9 +116,9 @@ class PlaylistView extends HookConsumerWidget {
|
||||
onPressed: () async {
|
||||
try {
|
||||
isFollowing
|
||||
? spotify.playlists
|
||||
? await spotify.playlists
|
||||
.unfollowPlaylist(playlist.id!)
|
||||
: spotify.playlists
|
||||
: await spotify.playlists
|
||||
.followPlaylist(playlist.id!);
|
||||
} catch (e, stack) {
|
||||
logger.e("FollowButton.onPressed", e, stack);
|
||||
@ -100,71 +135,8 @@ class PlaylistView extends HookConsumerWidget {
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
|
||||
if (playlist.id != "user-liked-tracks")
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
onPressed: () {
|
||||
final data =
|
||||
"https://open.spotify.com/playlist/${playlist.id}";
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: data),
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
"Copied $data to clipboard",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: tracksSnapshot.asData?.value != null
|
||||
? () => playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(playlist.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
tracksSnapshot.when(
|
||||
data: (tracks) {
|
||||
return TracksTableView(
|
||||
tracks,
|
||||
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
||||
playback,
|
||||
tracks,
|
||||
currentTrack: currentTrack,
|
||||
),
|
||||
playlistId: playlist.id,
|
||||
userPlaylist: playlist.owner?.id != null &&
|
||||
playlist.owner!.id == meSnapshot.asData?.value.id,
|
||||
);
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const ShimmerTrackTile(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
: null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,11 @@ class HeartButton extends StatelessWidget {
|
||||
final bool isLiked;
|
||||
final void Function() onPressed;
|
||||
final IconData? icon;
|
||||
final Color? color;
|
||||
const HeartButton({
|
||||
required this.isLiked,
|
||||
required this.onPressed,
|
||||
this.color,
|
||||
this.icon,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
@ -19,7 +21,7 @@ class HeartButton extends StatelessWidget {
|
||||
(!isLiked
|
||||
? Icons.favorite_outline_rounded
|
||||
: Icons.favorite_rounded),
|
||||
color: isLiked ? Theme.of(context).primaryColor : null,
|
||||
color: isLiked ? Theme.of(context).primaryColor : color,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
|
30
lib/components/Shared/NotFound.dart
Normal file
30
lib/components/Shared/NotFound.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class NotFound extends StatelessWidget {
|
||||
const NotFound({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 150,
|
||||
width: 150,
|
||||
child: Image.asset("assets/empty_box.png"),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Nothing found", style: Theme.of(context).textTheme.headline6),
|
||||
Text(
|
||||
"The box is empty",
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -4,7 +4,11 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TitleBarActionButtons extends StatelessWidget {
|
||||
const TitleBarActionButtons({Key? key}) : super(key: key);
|
||||
final Color? color;
|
||||
const TitleBarActionButtons({
|
||||
Key? key,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -18,7 +22,10 @@ class TitleBarActionButtons extends StatelessWidget {
|
||||
foregroundColor:
|
||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
||||
),
|
||||
child: const Icon(Icons.minimize_rounded)),
|
||||
child: Icon(
|
||||
Icons.minimize_rounded,
|
||||
color: color,
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
appWindow.maximizeOrRestore();
|
||||
@ -27,14 +34,14 @@ class TitleBarActionButtons extends StatelessWidget {
|
||||
foregroundColor:
|
||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
||||
),
|
||||
child: const Icon(Icons.crop_square_rounded)),
|
||||
child: Icon(Icons.crop_square_rounded, color: color)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
appWindow.close();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
foregroundColor:
|
||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
color ?? Theme.of(context).iconTheme.color),
|
||||
overlayColor: MaterialStateProperty.all(Colors.redAccent),
|
||||
),
|
||||
child: const Icon(
|
||||
@ -49,12 +56,14 @@ class PageWindowTitleBar extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
final Widget? leading;
|
||||
final Widget? center;
|
||||
final bool transparent;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
const PageWindowTitleBar({
|
||||
Key? key,
|
||||
this.leading,
|
||||
this.center,
|
||||
this.transparent = false,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
}) : super(key: key);
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(
|
||||
@ -76,7 +85,7 @@ class PageWindowTitleBar extends StatelessWidget
|
||||
}
|
||||
return WindowTitleBarBox(
|
||||
child: Container(
|
||||
color: !transparent ? Theme.of(context).scaffoldBackgroundColor : null,
|
||||
color: backgroundColor,
|
||||
child: Row(
|
||||
children: [
|
||||
if (Platform.isMacOS)
|
||||
@ -86,7 +95,7 @@ class PageWindowTitleBar extends StatelessWidget
|
||||
if (leading != null) leading!,
|
||||
Expanded(child: MoveWindow(child: Center(child: center))),
|
||||
if (!Platform.isMacOS && !Platform.isIOS && !Platform.isAndroid)
|
||||
const TitleBarActionButtons()
|
||||
TitleBarActionButtons(color: foregroundColor)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
240
lib/components/Shared/TrackCollectionView.dart
Normal file
240
lib/components/Shared/TrackCollectionView.dart
Normal file
@ -0,0 +1,240 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
class TrackCollectionView extends HookConsumerWidget {
|
||||
final logger = getLogger(TrackCollectionView);
|
||||
final String id;
|
||||
final String title;
|
||||
final String? description;
|
||||
final AsyncValue<List<TrackSimple>> tracksSnapshot;
|
||||
final String titleImage;
|
||||
final bool isPlaying;
|
||||
final void Function([Track? currentTrack]) onPlay;
|
||||
final void Function() onShare;
|
||||
final Widget? heartBtn;
|
||||
final AlbumSimple? album;
|
||||
|
||||
final bool showShare;
|
||||
final bool isOwned;
|
||||
TrackCollectionView({
|
||||
required this.title,
|
||||
required this.id,
|
||||
required this.tracksSnapshot,
|
||||
required this.titleImage,
|
||||
required this.isPlaying,
|
||||
required this.onPlay,
|
||||
required this.onShare,
|
||||
this.heartBtn,
|
||||
this.album,
|
||||
this.description,
|
||||
this.showShare = true,
|
||||
this.isOwned = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final color = usePaletteGenerator(
|
||||
context,
|
||||
titleImage,
|
||||
).dominantColor;
|
||||
|
||||
final List<Widget> buttons = [
|
||||
if (showShare)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.share_rounded,
|
||||
color: color?.titleTextColor,
|
||||
),
|
||||
onPressed: onShare,
|
||||
),
|
||||
if (heartBtn != null) heartBtn!,
|
||||
|
||||
// play playlist
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Theme.of(context).primaryColor),
|
||||
shape: MaterialStateProperty.all(
|
||||
const CircleBorder(),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
isPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).backgroundColor,
|
||||
),
|
||||
onPressed: tracksSnapshot.asData?.value != null ? onPlay : null,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final controller = useScrollController();
|
||||
|
||||
final collapsed = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
listener() {
|
||||
if (controller.position.pixels >= 400 && !collapsed.value) {
|
||||
collapsed.value = true;
|
||||
} else if (controller.position.pixels < 400 && collapsed.value) {
|
||||
collapsed.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
controller.addListener(listener);
|
||||
|
||||
return () => controller.removeListener(listener);
|
||||
}, [collapsed.value]);
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
backgroundColor:
|
||||
tracksSnapshot.asData?.value != null ? color?.color : null,
|
||||
foregroundColor: tracksSnapshot.asData?.value != null
|
||||
? color?.titleTextColor
|
||||
: null,
|
||||
leading: Row(
|
||||
children: [
|
||||
BackButton(
|
||||
color: tracksSnapshot.asData?.value != null
|
||||
? color?.titleTextColor
|
||||
: null,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
body: tracksSnapshot.when(
|
||||
data: (tracks) {
|
||||
return CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
actions: collapsed.value ? buttons : null,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
expandedHeight: 400,
|
||||
automaticallyImplyLeading: false,
|
||||
primary: true,
|
||||
title: collapsed.value
|
||||
? Text(
|
||||
title,
|
||||
style:
|
||||
Theme.of(context).textTheme.headline4?.copyWith(
|
||||
color: color?.titleTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
backgroundColor: color?.color.withOpacity(0.8),
|
||||
flexibleSpace: LayoutBuilder(builder: (context, constrains) {
|
||||
return FlexibleSpaceBar(
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
color?.color ?? Colors.transparent,
|
||||
Theme.of(context).canvasColor,
|
||||
],
|
||||
begin: const FractionalOffset(0, 0),
|
||||
end: const FractionalOffset(0, 1),
|
||||
tileMode: TileMode.clamp,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 20,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
constraints:
|
||||
const BoxConstraints(maxHeight: 200),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: titleImage,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline4
|
||||
?.copyWith(
|
||||
color: color?.titleTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (description != null)
|
||||
Text(
|
||||
description!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(
|
||||
color: color?.bodyTextColor,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: buttons,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
TracksTableView(
|
||||
tracks is! List<Track>
|
||||
? tracks
|
||||
.map((track) => simpleTrackToTrack(track, album!))
|
||||
.toList()
|
||||
: tracks,
|
||||
onTrackPlayButtonPressed: onPlay,
|
||||
playlistId: id,
|
||||
userPlaylist: isOwned,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const ShimmerTrackTile(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -12,12 +12,15 @@ class TracksTableView extends HookConsumerWidget {
|
||||
final List<Track> tracks;
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
|
||||
final Widget? heading;
|
||||
const TracksTableView(
|
||||
this.tracks, {
|
||||
Key? key,
|
||||
this.onTrackPlayButtonPressed,
|
||||
this.userPlaylist = false,
|
||||
this.playlistId,
|
||||
this.heading,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -28,10 +31,79 @@ class TracksTableView extends HookConsumerWidget {
|
||||
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
return Expanded(
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (heading != null) heading!,
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"#",
|
||||
textAlign: TextAlign.center,
|
||||
style: tableHeadStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Title",
|
||||
style: tableHeadStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// used alignment of this table-head
|
||||
if (breakpoint.isMoreThan(Breakpoints.md)) ...[
|
||||
const SizedBox(width: 100),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Album",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: tableHeadStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
if (!breakpoint.isSm) ...[
|
||||
const SizedBox(width: 10),
|
||||
Text("Time", style: tableHeadStyle),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
const SizedBox(width: 40),
|
||||
],
|
||||
),
|
||||
...tracks.asMap().entries.map((track) {
|
||||
String? thumbnailUrl = imageToUrlString(
|
||||
track.value.album?.images,
|
||||
index: (track.value.album?.images?.length ?? 1) - 1,
|
||||
);
|
||||
String duration =
|
||||
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||
return TrackTile(
|
||||
playback,
|
||||
playlistId: playlistId,
|
||||
track: track,
|
||||
duration: duration,
|
||||
thumbnailUrl: thumbnailUrl,
|
||||
userPlaylist: userPlaylist,
|
||||
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
|
||||
);
|
||||
}).toList()
|
||||
]),
|
||||
);
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: Scrollbar(
|
||||
child: ListView(
|
||||
children: [
|
||||
if (heading != null) heading!,
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
|
@ -38,3 +38,30 @@ PaletteColor usePaletteColor(
|
||||
|
||||
return paletteColor;
|
||||
}
|
||||
|
||||
PaletteGenerator usePaletteGenerator(
|
||||
BuildContext context,
|
||||
String imageUrl,
|
||||
) {
|
||||
final palette = useState(PaletteGenerator.fromColors([]));
|
||||
final mounted = useIsMounted();
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final newPalette = await PaletteGenerator.fromImageProvider(
|
||||
CachedNetworkImageProvider(
|
||||
imageUrl,
|
||||
cacheKey: imageUrl,
|
||||
maxHeight: 50,
|
||||
maxWidth: 50,
|
||||
),
|
||||
);
|
||||
if (!mounted()) return;
|
||||
|
||||
palette.value = newPalette;
|
||||
});
|
||||
return null;
|
||||
}, [imageUrl]);
|
||||
|
||||
return palette.value;
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ class Auth extends PersistedChangeNotifier {
|
||||
_refreshToken = null;
|
||||
_expiration = null;
|
||||
notifyListeners();
|
||||
updatePersistence();
|
||||
updatePersistence(clearNullEntries: true);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/helpers/getLyrics.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/timed-lyrics.dart';
|
||||
import 'package:spotube/models/SpotubeTrack.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
@ -118,9 +119,18 @@ final albumTracksQuery = FutureProvider.family<List<TrackSimple>, String>(
|
||||
);
|
||||
|
||||
final currentUserQuery = FutureProvider<User>(
|
||||
(ref) {
|
||||
(ref) async {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
return spotify.me.get();
|
||||
final me = await spotify.me.get();
|
||||
if (me.images == null || me.images?.isEmpty == true) {
|
||||
me.images = [
|
||||
Image()
|
||||
..height = 50
|
||||
..width = 50
|
||||
..url = imageToUrlString(me.images),
|
||||
];
|
||||
}
|
||||
return me;
|
||||
},
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user