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/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.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/HeartButton.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
@ -41,7 +41,6 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
|
|
||||||
final isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
|
|
||||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||||
final Auth auth = ref.watch(authProvider);
|
final Auth auth = ref.watch(authProvider);
|
||||||
|
|
||||||
@ -49,85 +48,60 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
final albumSavedSnapshot =
|
final albumSavedSnapshot =
|
||||||
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
||||||
|
|
||||||
return SafeArea(
|
final albumArt =
|
||||||
child: Scaffold(
|
useMemoized(() => imageToUrlString(album.images), [album.images]);
|
||||||
body: Column(
|
|
||||||
children: [
|
return TrackCollectionView(
|
||||||
PageWindowTitleBar(
|
id: album.id!,
|
||||||
leading: Row(
|
isPlaying: playback.currentPlaylist?.id != null &&
|
||||||
children: [
|
playback.currentPlaylist?.id == album.id,
|
||||||
// nav back
|
title: album.name!,
|
||||||
const BackButton(),
|
titleImage: albumArt,
|
||||||
// heart playlist
|
tracksSnapshot: tracksSnapshot,
|
||||||
if (auth.isLoggedIn)
|
album: album,
|
||||||
albumSavedSnapshot.when(
|
onPlay: ([track]) {
|
||||||
data: (isSaved) {
|
if (tracksSnapshot.asData?.value != null) {
|
||||||
return HeartButton(
|
playPlaylist(
|
||||||
isLiked: isSaved,
|
playback,
|
||||||
onPressed: () {
|
tracksSnapshot.asData!.value
|
||||||
(isSaved
|
.map((track) => simpleTrackToTrack(track, album))
|
||||||
? spotify.me.removeAlbums(
|
.toList(),
|
||||||
[album.id!],
|
currentTrack: track,
|
||||||
)
|
);
|
||||||
: spotify.me.saveAlbums(
|
}
|
||||||
[album.id!],
|
},
|
||||||
))
|
onShare: () {
|
||||||
.whenComplete(() {
|
Clipboard.setData(
|
||||||
ref.refresh(
|
ClipboardData(text: "https://open.spotify.com/album/${album.id}"),
|
||||||
albumIsSavedForCurrentUserQuery(
|
);
|
||||||
album.id!,
|
},
|
||||||
),
|
heartBtn: auth.isLoggedIn
|
||||||
);
|
? albumSavedSnapshot.when(
|
||||||
ref.refresh(currentUserAlbumsQuery);
|
data: (isSaved) {
|
||||||
});
|
return HeartButton(
|
||||||
},
|
isLiked: isSaved,
|
||||||
);
|
onPressed: () {
|
||||||
},
|
(isSaved
|
||||||
error: (error, _) => Text("Error $error"),
|
? spotify.me.removeAlbums(
|
||||||
loading: () => const CircularProgressIndicator()),
|
[album.id!],
|
||||||
// play playlist
|
)
|
||||||
IconButton(
|
: spotify.me.saveAlbums(
|
||||||
icon: Icon(
|
[album.id!],
|
||||||
isPlaylistPlaying
|
))
|
||||||
? Icons.stop_rounded
|
.whenComplete(() {
|
||||||
: Icons.play_arrow_rounded,
|
ref.refresh(
|
||||||
),
|
albumIsSavedForCurrentUserQuery(
|
||||||
onPressed: tracksSnapshot.asData?.value != null
|
album.id!,
|
||||||
? () => playPlaylist(
|
),
|
||||||
playback,
|
);
|
||||||
tracksSnapshot.asData!.value.map((trackSmp) {
|
ref.refresh(currentUserAlbumsQuery);
|
||||||
return simpleTrackToTrack(trackSmp, album);
|
});
|
||||||
}).toList(),
|
},
|
||||||
)
|
|
||||||
: 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"),
|
error: (error, _) => Text("Error $error"),
|
||||||
loading: () => const ShimmerTrackTile(),
|
loading: () => const CircularProgressIndicator())
|
||||||
),
|
: null,
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||||
|
import 'package:spotube/components/Shared/NotFound.dart';
|
||||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
@ -72,6 +73,9 @@ class CategoryCard extends HookConsumerWidget {
|
|||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
|
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
|
||||||
|
noItemsFoundIndicatorBuilder: (context) {
|
||||||
|
return const NotFound();
|
||||||
|
},
|
||||||
firstPageProgressIndicatorBuilder: (context) {
|
firstPageProgressIndicatorBuilder: (context) {
|
||||||
return const ShimmerPlaybuttonCard();
|
return const ShimmerPlaybuttonCard();
|
||||||
},
|
},
|
||||||
|
@ -3,11 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.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:flutter/material.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/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/sideBarTiles.dart';
|
import 'package:spotube/models/sideBarTiles.dart';
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
@ -36,7 +35,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
final breakpoints = useBreakpoints();
|
final breakpoints = useBreakpoints();
|
||||||
if (breakpoints.isSm) return Container();
|
if (breakpoints.isSm) return Container();
|
||||||
final extended = useState(false);
|
final extended = useState(false);
|
||||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
final meSnapshot = ref.watch(currentUserQuery);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (breakpoints.isMd && extended.value) {
|
if (breakpoints.isMd && extended.value) {
|
||||||
@ -78,11 +77,10 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
: _buildSmallLogo(),
|
: _buildSmallLogo(),
|
||||||
trailing: FutureBuilder<User>(
|
trailing: meSnapshot.when(
|
||||||
future: spotify.me.get(),
|
data: (data) {
|
||||||
builder: (context, snapshot) {
|
final avatarImg = imageToUrlString(data.images,
|
||||||
final avatarImg = imageToUrlString(snapshot.data?.images,
|
index: (data.images?.length ?? 1) - 1);
|
||||||
index: (snapshot.data?.images?.length ?? 1) - 1);
|
|
||||||
return extended.value
|
return extended.value
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@ -97,7 +95,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
snapshot.data?.displayName ?? "Guest",
|
data.displayName ?? "Guest",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
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/material.dart' hide Image;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/components/Album/AlbumCard.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/helpers/simple-album-to-album.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.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"),
|
error: (_, __) => const Text("Failure is the pillar of success"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart' hide Image;
|
import 'package:flutter/material.dart' hide Image;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.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/PlaylistCard.dart';
|
||||||
import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart';
|
import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
@ -13,8 +14,7 @@ class UserPlaylists extends ConsumerWidget {
|
|||||||
final playlists = ref.watch(currentUserPlaylistsQuery);
|
final playlists = ref.watch(currentUserPlaylistsQuery);
|
||||||
|
|
||||||
return playlists.when(
|
return playlists.when(
|
||||||
loading: () =>
|
loading: () => const Center(child: ShimmerPlaybuttonCard(count: 7)),
|
||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
|
||||||
data: (data) {
|
data: (data) {
|
||||||
Image image = Image();
|
Image image = Image();
|
||||||
image.height = 300;
|
image.height = 300;
|
||||||
|
@ -37,56 +37,65 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
right: (breakpoint.isMd ? 10 : 5),
|
right: (breakpoint.isMd ? 10 : 5),
|
||||||
left: (breakpoint.isSm ? 5 : 80),
|
left: (breakpoint.isSm ? 5 : 80),
|
||||||
bottom: (breakpoint.isSm ? 63 : 10),
|
bottom: (breakpoint.isSm ? 63 : 10),
|
||||||
child: AnimatedContainer(
|
child: GestureDetector(
|
||||||
duration: const Duration(milliseconds: 500),
|
onVerticalDragEnd: (details) {
|
||||||
width: MediaQuery.of(context).size.width,
|
int sensitivity = 8;
|
||||||
height: 50,
|
if (details.primaryVelocity != null &&
|
||||||
decoration: BoxDecoration(
|
details.primaryVelocity! < -sensitivity) {
|
||||||
color: paletteColor.color,
|
GoRouter.of(context).push("/player");
|
||||||
borderRadius: BorderRadius.circular(5),
|
}
|
||||||
),
|
},
|
||||||
child: Material(
|
child: AnimatedContainer(
|
||||||
type: MaterialType.transparency,
|
duration: const Duration(milliseconds: 500),
|
||||||
child: Row(
|
width: MediaQuery.of(context).size.width,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
height: 50,
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
Expanded(
|
color: paletteColor.color,
|
||||||
child: MouseRegion(
|
borderRadius: BorderRadius.circular(5),
|
||||||
cursor: SystemMouseCursors.click,
|
),
|
||||||
child: GestureDetector(
|
child: Material(
|
||||||
onTap: () => GoRouter.of(context).push("/player"),
|
type: MaterialType.transparency,
|
||||||
child: PlayerTrackDetails(
|
child: Row(
|
||||||
albumArt: albumArt,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
color: paletteColor.bodyTextColor,
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => GoRouter.of(context).push("/player"),
|
||||||
|
child: PlayerTrackDetails(
|
||||||
|
albumArt: albumArt,
|
||||||
|
color: paletteColor.bodyTextColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Row(
|
||||||
Row(
|
children: [
|
||||||
children: [
|
IconButton(
|
||||||
IconButton(
|
icon: const Icon(Icons.skip_previous_rounded),
|
||||||
icon: const Icon(Icons.skip_previous_rounded),
|
color: paletteColor.bodyTextColor,
|
||||||
|
onPressed: () {
|
||||||
|
onPrevious();
|
||||||
|
}),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
playback.isPlaying
|
||||||
|
? Icons.pause_rounded
|
||||||
|
: Icons.play_arrow_rounded,
|
||||||
|
),
|
||||||
color: paletteColor.bodyTextColor,
|
color: paletteColor.bodyTextColor,
|
||||||
onPressed: () {
|
onPressed: _playOrPause,
|
||||||
onPrevious();
|
|
||||||
}),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
playback.isPlaying
|
|
||||||
? Icons.pause_rounded
|
|
||||||
: Icons.play_arrow_rounded,
|
|
||||||
),
|
),
|
||||||
color: paletteColor.bodyTextColor,
|
IconButton(
|
||||||
onPressed: _playOrPause,
|
icon: const Icon(Icons.skip_next_rounded),
|
||||||
),
|
onPressed: () => onNext(),
|
||||||
IconButton(
|
color: paletteColor.bodyTextColor,
|
||||||
icon: const Icon(Icons.skip_next_rounded),
|
),
|
||||||
onPressed: () => onNext(),
|
],
|
||||||
color: paletteColor.bodyTextColor,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -74,7 +74,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
leading: BackButton(),
|
leading: BackButton(),
|
||||||
transparent: true,
|
backgroundColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
backgroundColor: paletteColor.color,
|
backgroundColor: paletteColor.color,
|
||||||
body: Column(
|
body: Column(
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/HeartButton.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
|
||||||
import 'package:spotube/helpers/image-to-url-string.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/CurrentPlaylist.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
@ -53,118 +53,90 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
final meSnapshot = ref.watch(currentUserQuery);
|
final meSnapshot = ref.watch(currentUserQuery);
|
||||||
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
||||||
|
|
||||||
return SafeArea(
|
final titleImage =
|
||||||
child: Scaffold(
|
useMemoized(() => imageToUrlString(playlist.images), [playlist.images]);
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
PageWindowTitleBar(
|
|
||||||
leading: Row(
|
|
||||||
children: [
|
|
||||||
// nav back
|
|
||||||
const BackButton(),
|
|
||||||
// heart playlist
|
|
||||||
if (auth.isLoggedIn && playlist.id != "user-liked-tracks")
|
|
||||||
meSnapshot.when(
|
|
||||||
data: (me) {
|
|
||||||
final query = playlistIsFollowedQuery(jsonEncode(
|
|
||||||
{"playlistId": playlist.id, "userId": me.id!}));
|
|
||||||
final followingSnapshot = ref.watch(query);
|
|
||||||
|
|
||||||
return followingSnapshot.when(
|
final color = usePaletteGenerator(
|
||||||
data: (isFollowing) {
|
context,
|
||||||
return HeartButton(
|
titleImage,
|
||||||
isLiked: isFollowing,
|
).dominantColor;
|
||||||
icon: playlist.owner?.id != null &&
|
|
||||||
me.id == playlist.owner?.id
|
|
||||||
? Icons.delete_outline_rounded
|
|
||||||
: null,
|
|
||||||
onPressed: () async {
|
|
||||||
try {
|
|
||||||
isFollowing
|
|
||||||
? spotify.playlists
|
|
||||||
.unfollowPlaylist(playlist.id!)
|
|
||||||
: spotify.playlists
|
|
||||||
.followPlaylist(playlist.id!);
|
|
||||||
} catch (e, stack) {
|
|
||||||
logger.e("FollowButton.onPressed", e, stack);
|
|
||||||
} finally {
|
|
||||||
ref.refresh(query);
|
|
||||||
ref.refresh(currentUserPlaylistsQuery);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (error, _) => Text("Error $error"),
|
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (error, _) => Text("Error $error"),
|
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (playlist.id != "user-liked-tracks")
|
return TrackCollectionView(
|
||||||
IconButton(
|
id: playlist.id!,
|
||||||
icon: const Icon(Icons.share_rounded),
|
isPlaying: isPlaylistPlaying,
|
||||||
onPressed: () {
|
title: playlist.name!,
|
||||||
final data =
|
titleImage: titleImage,
|
||||||
"https://open.spotify.com/playlist/${playlist.id}";
|
tracksSnapshot: tracksSnapshot,
|
||||||
Clipboard.setData(
|
description: playlist.description,
|
||||||
ClipboardData(text: data),
|
isOwned: playlist.owner?.id != null &&
|
||||||
).then((_) {
|
playlist.owner!.id == meSnapshot.asData?.value.id,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
onPlay: ([track]) {
|
||||||
SnackBar(
|
if (tracksSnapshot.asData?.value != null) {
|
||||||
width: 300,
|
playPlaylist(
|
||||||
behavior: SnackBarBehavior.floating,
|
playback,
|
||||||
content: Text(
|
tracksSnapshot.asData!.value,
|
||||||
"Copied $data to clipboard",
|
currentTrack: track,
|
||||||
textAlign: TextAlign.center,
|
);
|
||||||
),
|
}
|
||||||
),
|
},
|
||||||
);
|
showShare: playlist.id != "user-liked-tracks",
|
||||||
});
|
onShare: () {
|
||||||
},
|
final data = "https://open.spotify.com/playlist/${playlist.id}";
|
||||||
),
|
Clipboard.setData(
|
||||||
// play playlist
|
ClipboardData(text: data),
|
||||||
IconButton(
|
).then((_) {
|
||||||
icon: Icon(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
isPlaylistPlaying
|
SnackBar(
|
||||||
? Icons.stop_rounded
|
width: 300,
|
||||||
: Icons.play_arrow_rounded,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
content: Text(
|
||||||
onPressed: tracksSnapshot.asData?.value != null
|
"Copied $data to clipboard",
|
||||||
? () => playPlaylist(
|
textAlign: TextAlign.center,
|
||||||
playback,
|
|
||||||
tracksSnapshot.asData!.value,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Center(
|
);
|
||||||
child: Text(playlist.name!,
|
});
|
||||||
style: Theme.of(context).textTheme.headline4),
|
},
|
||||||
),
|
heartBtn: (auth.isLoggedIn && playlist.id != "user-liked-tracks"
|
||||||
tracksSnapshot.when(
|
? meSnapshot.when(
|
||||||
data: (tracks) {
|
data: (me) {
|
||||||
return TracksTableView(
|
final query = playlistIsFollowedQuery(
|
||||||
tracks,
|
jsonEncode({"playlistId": playlist.id, "userId": me.id!}));
|
||||||
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
final followingSnapshot = ref.watch(query);
|
||||||
playback,
|
|
||||||
tracks,
|
return followingSnapshot.when(
|
||||||
currentTrack: currentTrack,
|
data: (isFollowing) {
|
||||||
),
|
return HeartButton(
|
||||||
playlistId: playlist.id,
|
isLiked: isFollowing,
|
||||||
userPlaylist: playlist.owner?.id != null &&
|
color: color?.titleTextColor,
|
||||||
playlist.owner!.id == meSnapshot.asData?.value.id,
|
icon: playlist.owner?.id != null &&
|
||||||
|
me.id == playlist.owner?.id
|
||||||
|
? Icons.delete_outline_rounded
|
||||||
|
: null,
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
isFollowing
|
||||||
|
? await spotify.playlists
|
||||||
|
.unfollowPlaylist(playlist.id!)
|
||||||
|
: await spotify.playlists
|
||||||
|
.followPlaylist(playlist.id!);
|
||||||
|
} catch (e, stack) {
|
||||||
|
logger.e("FollowButton.onPressed", e, stack);
|
||||||
|
} finally {
|
||||||
|
ref.refresh(query);
|
||||||
|
ref.refresh(currentUserPlaylistsQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, _) => Text("Error $error"),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error: (error, _) => Text("Error $error"),
|
error: (error, _) => Text("Error $error"),
|
||||||
loading: () => const ShimmerTrackTile(),
|
loading: () => const CircularProgressIndicator(),
|
||||||
),
|
)
|
||||||
],
|
: null),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ class HeartButton extends StatelessWidget {
|
|||||||
final bool isLiked;
|
final bool isLiked;
|
||||||
final void Function() onPressed;
|
final void Function() onPressed;
|
||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
|
final Color? color;
|
||||||
const HeartButton({
|
const HeartButton({
|
||||||
required this.isLiked,
|
required this.isLiked,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
|
this.color,
|
||||||
this.icon,
|
this.icon,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@ -19,7 +21,7 @@ class HeartButton extends StatelessWidget {
|
|||||||
(!isLiked
|
(!isLiked
|
||||||
? Icons.favorite_outline_rounded
|
? Icons.favorite_outline_rounded
|
||||||
: Icons.favorite_rounded),
|
: Icons.favorite_rounded),
|
||||||
color: isLiked ? Theme.of(context).primaryColor : null,
|
color: isLiked ? Theme.of(context).primaryColor : color,
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TitleBarActionButtons extends StatelessWidget {
|
class TitleBarActionButtons extends StatelessWidget {
|
||||||
const TitleBarActionButtons({Key? key}) : super(key: key);
|
final Color? color;
|
||||||
|
const TitleBarActionButtons({
|
||||||
|
Key? key,
|
||||||
|
this.color,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -18,7 +22,10 @@ class TitleBarActionButtons extends StatelessWidget {
|
|||||||
foregroundColor:
|
foregroundColor:
|
||||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.minimize_rounded)),
|
child: Icon(
|
||||||
|
Icons.minimize_rounded,
|
||||||
|
color: color,
|
||||||
|
)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
appWindow.maximizeOrRestore();
|
appWindow.maximizeOrRestore();
|
||||||
@ -27,14 +34,14 @@ class TitleBarActionButtons extends StatelessWidget {
|
|||||||
foregroundColor:
|
foregroundColor:
|
||||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.crop_square_rounded)),
|
child: Icon(Icons.crop_square_rounded, color: color)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
appWindow.close();
|
appWindow.close();
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
foregroundColor:
|
foregroundColor: MaterialStateProperty.all(
|
||||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
color ?? Theme.of(context).iconTheme.color),
|
||||||
overlayColor: MaterialStateProperty.all(Colors.redAccent),
|
overlayColor: MaterialStateProperty.all(Colors.redAccent),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@ -49,12 +56,14 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
implements PreferredSizeWidget {
|
implements PreferredSizeWidget {
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
final Widget? center;
|
final Widget? center;
|
||||||
final bool transparent;
|
final Color? backgroundColor;
|
||||||
|
final Color? foregroundColor;
|
||||||
const PageWindowTitleBar({
|
const PageWindowTitleBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.leading,
|
this.leading,
|
||||||
this.center,
|
this.center,
|
||||||
this.transparent = false,
|
this.backgroundColor,
|
||||||
|
this.foregroundColor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => Size.fromHeight(
|
Size get preferredSize => Size.fromHeight(
|
||||||
@ -76,7 +85,7 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
}
|
}
|
||||||
return WindowTitleBarBox(
|
return WindowTitleBarBox(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: !transparent ? Theme.of(context).scaffoldBackgroundColor : null,
|
color: backgroundColor,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (Platform.isMacOS)
|
if (Platform.isMacOS)
|
||||||
@ -86,7 +95,7 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
if (leading != null) leading!,
|
if (leading != null) leading!,
|
||||||
Expanded(child: MoveWindow(child: Center(child: center))),
|
Expanded(child: MoveWindow(child: Center(child: center))),
|
||||||
if (!Platform.isMacOS && !Platform.isIOS && !Platform.isAndroid)
|
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 List<Track> tracks;
|
||||||
final bool userPlaylist;
|
final bool userPlaylist;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
|
|
||||||
|
final Widget? heading;
|
||||||
const TracksTableView(
|
const TracksTableView(
|
||||||
this.tracks, {
|
this.tracks, {
|
||||||
Key? key,
|
Key? key,
|
||||||
this.onTrackPlayButtonPressed,
|
this.onTrackPlayButtonPressed,
|
||||||
this.userPlaylist = false,
|
this.userPlaylist = false,
|
||||||
this.playlistId,
|
this.playlistId,
|
||||||
|
this.heading,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,10 +31,79 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
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: Scrollbar(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
|
if (heading != null) heading!,
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -38,3 +38,30 @@ PaletteColor usePaletteColor(
|
|||||||
|
|
||||||
return paletteColor;
|
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;
|
_refreshToken = null;
|
||||||
_expiration = null;
|
_expiration = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence(clearNullEntries: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/helpers/getLyrics.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/helpers/timed-lyrics.dart';
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
@ -118,9 +119,18 @@ final albumTracksQuery = FutureProvider.family<List<TrackSimple>, String>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
final currentUserQuery = FutureProvider<User>(
|
final currentUserQuery = FutureProvider<User>(
|
||||||
(ref) {
|
(ref) async {
|
||||||
final spotify = ref.watch(spotifyProvider);
|
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