mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Added: SearchResult comp enhanced, Error, loading, retry handler comp, pagination
This commit is contained in:
parent
d695172804
commit
e91a218a8c
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
BIN
assets/loading-spinner.gif
Normal file
BIN
assets/loading-spinner.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
@ -53,7 +53,6 @@ function RootApp() {
|
||||
if (nativeEv) {
|
||||
const event = new QKeyEvent(nativeEv);
|
||||
const eventKey = event.key();
|
||||
console.log("eventKey:", eventKey);
|
||||
if (audioPlayer.isRunning() && currentTrack)
|
||||
switch (eventKey) {
|
||||
case 32: //space
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||
import React, { useContext } from "react";
|
||||
import playerContext from "../context/playerContext";
|
||||
import { TrackButton, TrackTableIndex } from "./PlaylistView";
|
||||
import { TrackTableIndex } from "./PlaylistView";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
|
||||
function CurrentPlaylist() {
|
||||
const { currentPlaylist, currentTrack, setCurrentTrack } = useContext(playerContext);
|
||||
const { currentPlaylist, currentTrack } = useContext(playerContext);
|
||||
|
||||
if (!currentPlaylist && !currentTrack) {
|
||||
return <Text style="flex: 1;">{`<center>There is nothing being played now</center>`}</Text>;
|
||||
}
|
||||
|
||||
if (currentTrack && !currentPlaylist) {
|
||||
<View style="flex: 1;">
|
||||
<TrackButton track={currentTrack} index={0}/>
|
||||
</View>
|
||||
}
|
||||
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text>{`<center><h2>${currentPlaylist?.name}</h2></center>`}</Text>
|
||||
@ -17,16 +24,7 @@ function CurrentPlaylist() {
|
||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column; flex: 1;`}>
|
||||
{currentPlaylist?.tracks.map(({ track }, index) => {
|
||||
return (
|
||||
<TrackButton
|
||||
key={index + track.id}
|
||||
active={currentTrack?.id === track.id}
|
||||
track={track}
|
||||
index={index}
|
||||
on={{ MouseButtonRelease: () => setCurrentTrack(track) }}
|
||||
onTrackClick={() => {}}
|
||||
/>
|
||||
);
|
||||
return <TrackButton key={index + track.id} track={track} index={index} />;
|
||||
})}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
|
@ -1,21 +1,14 @@
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
import { Button, ScrollArea, View, Text } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
import { Button, ScrollArea, View } from "@nodegui/react-nodegui";
|
||||
import { useHistory } from "react-router";
|
||||
import CachedImage from "./shared/CachedImage";
|
||||
import { CursorShape, QIcon, QMouseEvent } from "@nodegui/nodegui";
|
||||
import { CursorShape, QMouseEvent } from "@nodegui/nodegui";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import ErrorApplet from "./shared/ErrorApplet";
|
||||
import IconButton from "./shared/IconButton";
|
||||
import { heart, heartRegular, pause, play } from "../icons";
|
||||
import playerContext from "../context/playerContext";
|
||||
import { audioPlayer } from "./Player";
|
||||
import showError from "../helpers/showError";
|
||||
import { generateRandomColor, getDarkenForeground } from "../helpers/RandomColor";
|
||||
import usePlaylistReaction from "../hooks/usePlaylistReaction";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import PlaylistCard from "./shared/PlaylistCard";
|
||||
|
||||
function Home() {
|
||||
const { data: categories, isError, isRefetchError, refetch } = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
|
||||
const { data: categories, isError, refetch, isLoading } = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
|
||||
QueryCacheKeys.categories,
|
||||
(spotifyApi) => spotifyApi.getCategories({ country: "US" }).then((categoriesReceived) => categoriesReceived.body.categories.items),
|
||||
{ initialData: [] }
|
||||
@ -24,7 +17,7 @@ function Home() {
|
||||
return (
|
||||
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}>
|
||||
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
|
||||
{(isError || isRefetchError) && <ErrorApplet message="Failed to query genres" reload={refetch} helps />}
|
||||
<PlaceholderApplet error={isError} message="Failed to query genres" reload={refetch} helps loading={isLoading} />
|
||||
{categories?.map((category, index) => {
|
||||
return <CategoryCard key={index + category.id} id={category.id} name={category.name} />;
|
||||
})}
|
||||
@ -95,110 +88,3 @@ const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface PlaylistCardProps {
|
||||
playlist: SpotifyApi.PlaylistObjectSimplified;
|
||||
}
|
||||
|
||||
export const PlaylistCard = React.memo(({ playlist }: PlaylistCardProps) => {
|
||||
const { id, description, name, images } = playlist;
|
||||
const history = useHistory();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } = useContext(playerContext);
|
||||
const { refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>([QueryCacheKeys.playlistTracks, id], (spotifyApi) => spotifyApi.getPlaylistTracks(id).then((track) => track.body.items), {
|
||||
initialData: [],
|
||||
enabled: false,
|
||||
});
|
||||
const { reactToPlaylist, isFavorite } = usePlaylistReaction();
|
||||
|
||||
const handlePlaylistPlayPause = async () => {
|
||||
try {
|
||||
const { data: tracks, isSuccess } = await refetch();
|
||||
if (currentPlaylist?.id !== id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ tracks, id, name, thumbnail: images[0].url });
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
await audioPlayer.stop();
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed adding playlist to queue]: ");
|
||||
}
|
||||
};
|
||||
|
||||
function gotoPlaylist(native?: any) {
|
||||
const key = new QMouseEvent(native);
|
||||
if (key.button() === 1) {
|
||||
history.push(`/playlist/${id}`, { name, thumbnail: images[0].url });
|
||||
}
|
||||
}
|
||||
|
||||
const bgColor1 = useMemo(() => generateRandomColor(), []);
|
||||
const color = useMemo(() => getDarkenForeground(bgColor1), [bgColor1]);
|
||||
|
||||
const playlistStyleSheet = `
|
||||
#playlist-container{
|
||||
width: 150px;
|
||||
flex-direction: column;
|
||||
padding: 5px;
|
||||
min-height: 150px;
|
||||
background-color: ${bgColor1};
|
||||
border-radius: 5px;
|
||||
}
|
||||
#playlist-container:hover{
|
||||
border: 1px solid green;
|
||||
}
|
||||
#playlist-container:clicked{
|
||||
border: 5px solid green;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<View
|
||||
id="playlist-container"
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
styleSheet={playlistStyleSheet}
|
||||
on={{
|
||||
MouseButtonRelease: gotoPlaylist,
|
||||
HoverEnter() {
|
||||
setHovered(true);
|
||||
},
|
||||
HoverLeave() {
|
||||
setHovered(false);
|
||||
},
|
||||
}}>
|
||||
{/* <CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} /> */}
|
||||
<Text style={`color: ${color};`} wordWrap on={{ MouseButtonRelease: gotoPlaylist }}>
|
||||
{`
|
||||
<center>
|
||||
<h3>${name}</h3>
|
||||
<p>${description}</p>
|
||||
</center>
|
||||
`}
|
||||
</Text>
|
||||
{(hovered || currentPlaylist?.id === id) && (
|
||||
<>
|
||||
<IconButton
|
||||
style={`position: absolute; bottom: 30px; left: '55%'; background-color: ${color};`}
|
||||
icon={new QIcon(isFavorite(id) ? heart : heartRegular)}
|
||||
on={{
|
||||
clicked() {
|
||||
reactToPlaylist(playlist);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={new QIcon(currentPlaylist?.id === id ? pause : play)}
|
||||
style={`position: absolute; bottom: 30px; left: '80%'; background-color: ${color};`}
|
||||
on={{
|
||||
clicked() {
|
||||
handlePlaylistPlayPause();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { ScrollArea, View } from "@nodegui/react-nodegui";
|
||||
import { Button, ScrollArea, View } from "@nodegui/react-nodegui";
|
||||
import React, { useContext } from "react";
|
||||
import { Redirect, Route } from "react-router";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import playerContext from "../context/playerContext";
|
||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import { PlaylistCard } from "./Home";
|
||||
import { PlaylistSimpleControls, TrackButton, TrackTableIndex } from "./PlaylistView";
|
||||
import { PlaylistSimpleControls, TrackTableIndex } from "./PlaylistView";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import PlaylistCard from "./shared/PlaylistCard";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
import { TabMenuItem } from "./TabMenu";
|
||||
|
||||
function Library() {
|
||||
@ -38,6 +41,7 @@ function UserPlaylists() {
|
||||
return (
|
||||
<ScrollArea style="flex: 1; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'row'; flex-wrap: 'wrap'; justify-content: 'space-evenly'; width: 330px; align-items: 'center';">
|
||||
<PlaceholderApplet error={isError} loading={isLoading} message="Failed querying spotify" reload={refetch} />
|
||||
{userPlaylists?.map((playlist, index) => (
|
||||
<PlaylistCard key={index + playlist.id} playlist={playlist} />
|
||||
))}
|
||||
@ -48,10 +52,23 @@ function UserPlaylists() {
|
||||
|
||||
function UserSavedTracks() {
|
||||
const userSavedPlaylistId = "user-saved-tracks";
|
||||
const { data: userTracks, isError, isLoading } = useSpotifyQuery<SpotifyApi.SavedTrackObject[]>(QueryCacheKeys.userSavedTracks, (spotifyApi) =>
|
||||
spotifyApi.getMySavedTracks({ limit: 50 }).then((tracks) => tracks.body.items)
|
||||
const { data: userSavedTracks, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(
|
||||
QueryCacheKeys.userSavedTracks,
|
||||
(spotifyApi, { pageParam }) => spotifyApi.getMySavedTracks({ limit: 50, offset: pageParam }).then((res) => res.body),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
|
||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack } = useContext(playerContext);
|
||||
|
||||
const userTracks = userSavedTracks?.pages
|
||||
?.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
||||
|
||||
function handlePlaylistPlayPause(index?: number) {
|
||||
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
|
||||
@ -63,26 +80,58 @@ function UserSavedTracks() {
|
||||
}
|
||||
}
|
||||
|
||||
const playlist: SpotifyApi.PlaylistObjectFull = {
|
||||
collaborative: false,
|
||||
description: "User Playlist",
|
||||
tracks: {
|
||||
items: [userTracks ?? []].map(
|
||||
(userTrack) =>
|
||||
(({
|
||||
...userTrack,
|
||||
added_by: "Me",
|
||||
is_local: false,
|
||||
added_at: Date.now(),
|
||||
} as unknown) as SpotifyApi.PlaylistTrackObject)
|
||||
),
|
||||
limit: 20,
|
||||
href: "",
|
||||
next: "",
|
||||
offset: 0,
|
||||
previous: "",
|
||||
total: 20,
|
||||
},
|
||||
external_urls: { spotify: "" },
|
||||
followers: { href: null, total: 2 },
|
||||
href: "",
|
||||
id: userSavedPlaylistId,
|
||||
images: [],
|
||||
name: "User saved track",
|
||||
owner: { external_urls: { spotify: "" }, href: "", id: "Me", type: "user", uri: "spotify:user:me", display_name: "User", followers: { href: null, total: 0 } },
|
||||
public: false,
|
||||
snapshot_id: userSavedPlaylistId + "snapshot",
|
||||
type: "playlist",
|
||||
uri: "spotify:user:me:saved-tracks",
|
||||
};
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === userSavedPlaylistId} />
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style="flex: 1; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
|
||||
{userTracks?.map(({ track }, index) => (
|
||||
<TrackButton
|
||||
key={index+track.id}
|
||||
active={currentPlaylist?.id === userSavedPlaylistId && currentTrack?.id === track.id}
|
||||
track={track}
|
||||
index={index}
|
||||
<PlaceholderApplet error={isError} loading={isLoading || isFetchingNextPage} message="Failed querying spotify" reload={refetch} />
|
||||
{userTracks?.map(({ track }, index) => track && <TrackButton key={index + track.id} track={track} index={index} playlist={playlist} />)}
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
style="flex-grow: 0; align-self: 'center';"
|
||||
text="Load more"
|
||||
on={{
|
||||
MouseButtonRelease() {
|
||||
setCurrentTrack(track);
|
||||
clicked() {
|
||||
fetchNextPage();
|
||||
},
|
||||
}}
|
||||
onTrackClick={()=>handlePlaylistPlayPause(index)}
|
||||
enabled={!isFetchingNextPage}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
|
@ -174,6 +174,8 @@ function Player(): ReactElement {
|
||||
async function stopPlayback() {
|
||||
try {
|
||||
if (playerRunning) {
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
await audioPlayer.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Direction, Orientation, QAbstractSliderSignals } from "@nodegui/nodegui";
|
||||
import { BoxView, Slider, Text, useEventHandler, View } from "@nodegui/react-nodegui";
|
||||
import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
|
||||
import { BoxView, Slider, Text, useEventHandler } from "@nodegui/react-nodegui";
|
||||
import NodeMpv from "node-mpv";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import playerContext from "../context/playerContext";
|
||||
@ -36,7 +35,6 @@ function PlayerProgressBar({ audioPlayer, totalDuration }: PlayerProgressBarProp
|
||||
|
||||
useEffect(() => {
|
||||
const progressListener = (seconds: number) => {
|
||||
console.log("seconds", seconds);
|
||||
setTrackTime(seconds);
|
||||
};
|
||||
const statusListener = ({ property }: import("node-mpv").StatusObject): void => {
|
||||
|
@ -5,18 +5,19 @@ import { useLocation, useParams } from "react-router";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import BackButton from "./BackButton";
|
||||
import { PlaylistCard } from "./Home";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import PlaylistCard from "./shared/PlaylistCard";
|
||||
|
||||
function PlaylistGenreView() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string }>();
|
||||
const { data: playlists } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
||||
const { data: playlists, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
||||
[QueryCacheKeys.genrePlaylists, id],
|
||||
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id).then((playlistsRes) => playlistsRes.body.playlists.items),
|
||||
{ initialData: [] }
|
||||
);
|
||||
|
||||
return <GenreView heading={location.state.name} playlists={playlists ?? []} />;
|
||||
return <GenreView isError={isError} isLoading={isLoading} refetch={refetch} heading={location.state.name} playlists={playlists ?? []} />;
|
||||
}
|
||||
|
||||
export default PlaylistGenreView;
|
||||
@ -26,9 +27,12 @@ interface GenreViewProps {
|
||||
playlists: SpotifyApi.PlaylistObjectSimplified[];
|
||||
loadMore?: QAbstractButtonSignals["clicked"];
|
||||
isLoadable?: boolean;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
refetch: Function;
|
||||
}
|
||||
|
||||
export function GenreView({ heading, playlists, loadMore, isLoadable }: GenreViewProps) {
|
||||
export function GenreView({ heading, playlists, loadMore, isLoadable, isError, isLoading, refetch }: GenreViewProps) {
|
||||
const playlistGenreViewStylesheet = `
|
||||
#genre-container{
|
||||
flex-direction: column;
|
||||
@ -56,6 +60,7 @@ export function GenreView({ heading, playlists, loadMore, isLoadable }: GenreVie
|
||||
<Text id="heading">{`<h2>${heading}</h2>`}</Text>
|
||||
<ScrollArea id="scroll-view">
|
||||
<View id="child-container">
|
||||
<PlaceholderApplet error={isError} loading={isLoading} reload={refetch} message={`Failed loading ${heading}'s playlists`} />
|
||||
{playlists?.map((playlist, index) => {
|
||||
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
||||
})}
|
||||
|
@ -1,18 +1,17 @@
|
||||
import React, { FC, useContext, useMemo } from "react";
|
||||
import { View, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
|
||||
import React, { FC, useContext } from "react";
|
||||
import { View, ScrollArea, Text } from "@nodegui/react-nodegui";
|
||||
import BackButton from "./BackButton";
|
||||
import { useLocation, useParams } from "react-router";
|
||||
import { QAbstractButtonSignals, QIcon, QMouseEvent, QWidgetSignals } from "@nodegui/nodegui";
|
||||
import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
|
||||
import { QIcon } from "@nodegui/nodegui";
|
||||
import playerContext from "../context/playerContext";
|
||||
import IconButton from "./shared/IconButton";
|
||||
import { heart, heartRegular, pause, play, stop } from "../icons";
|
||||
import { heart, heartRegular, play, stop } from "../icons";
|
||||
import { audioPlayer } from "./Player";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import useTrackReaction from "../hooks/useTrackReaction";
|
||||
import usePlaylistReaction from "../hooks/usePlaylistReaction";
|
||||
import { msToMinAndSec } from "../helpers/msToMin:sec";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
|
||||
export interface PlaylistTrackRes {
|
||||
name: string;
|
||||
@ -21,7 +20,7 @@ export interface PlaylistTrackRes {
|
||||
}
|
||||
|
||||
const PlaylistView: FC = () => {
|
||||
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } = useContext(playerContext);
|
||||
const params = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string; thumbnail: string }>();
|
||||
const { isFavorite, reactToPlaylist } = usePlaylistReaction();
|
||||
@ -34,10 +33,10 @@ const PlaylistView: FC = () => {
|
||||
{ initialData: [] }
|
||||
);
|
||||
|
||||
const handlePlaylistPlayPause = (index?: number) => {
|
||||
const handlePlaylistPlayPause = () => {
|
||||
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
||||
setCurrentTrack(tracks[index ?? 0].track);
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error));
|
||||
setCurrentTrack(undefined);
|
||||
@ -45,10 +44,6 @@ const PlaylistView: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const trackClickHandler = (track: SpotifyApi.TrackObjectFull) => {
|
||||
setCurrentTrack(track);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={`flex: 1; flex-direction: 'column';`}>
|
||||
<PlaylistSimpleControls
|
||||
@ -61,36 +56,10 @@ const PlaylistView: FC = () => {
|
||||
{<TrackTableIndex />}
|
||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column; flex: 1;`}>
|
||||
{isLoading && <Text>{`Loading Tracks...`}</Text>}
|
||||
{isError && (
|
||||
<>
|
||||
<Text>{`Failed to load ${location.state.name} tracks`}</Text>
|
||||
<Button
|
||||
on={{
|
||||
clicked() {
|
||||
refetch();
|
||||
},
|
||||
}}
|
||||
text="Retry"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<PlaceholderApplet error={isError} loading={isLoading} reload={refetch} message={`Failed retrieving ${location.state.name} tracks`} />
|
||||
{tracks?.map(({ track }, index) => {
|
||||
if (track) {
|
||||
return (
|
||||
<TrackButton
|
||||
key={index + track.id}
|
||||
track={track}
|
||||
index={index}
|
||||
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
|
||||
on={{
|
||||
MouseButtonRelease: () => trackClickHandler(track),
|
||||
}}
|
||||
onTrackClick={() => {
|
||||
handlePlaylistPlayPause(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <TrackButton key={index + track.id} track={track} index={index} playlist={playlist} />;
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
@ -99,62 +68,6 @@ const PlaylistView: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export interface TrackButtonProps {
|
||||
track: SpotifyApi.TrackObjectFull;
|
||||
on: Partial<QWidgetSignals | WidgetEventListeners>;
|
||||
onTrackClick?: QAbstractButtonSignals["clicked"];
|
||||
active: boolean;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const TrackButton: FC<TrackButtonProps> = ({ track, active, index, on, onTrackClick }) => {
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
|
||||
const duration = useMemo(() => msToMinAndSec(track.duration_ms), []);
|
||||
return (
|
||||
<View
|
||||
id={active ? "active" : "track-button"}
|
||||
styleSheet={trackButtonStyle}
|
||||
on={{
|
||||
...on,
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1) {
|
||||
(on as WidgetEventListeners).MouseButtonRelease();
|
||||
}
|
||||
},
|
||||
}}>
|
||||
<Text style="padding: 5px;">{index + 1}</Text>
|
||||
<View style="flex-direction: 'column'; width: '35%';">
|
||||
<Text>{`<h3>${track.name}</h3>`}</Text>
|
||||
<Text>{track.artists.map((artist) => artist.name).join(", ")}</Text>
|
||||
</View>
|
||||
<Text style="width: '25%';">{track.album.name}</Text>
|
||||
<Text style="width: '15%';">{duration}</Text>
|
||||
<View style="width: '15%'; padding: 5px; justify-content: 'space-around';">
|
||||
<IconButton
|
||||
icon={new QIcon(isFavorite(track.id) ? heart : heartRegular)}
|
||||
on={{
|
||||
clicked() {
|
||||
reactToTrack({ track, added_at: "" });
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton icon={new QIcon(active ? pause : play)} on={{ clicked: onTrackClick }} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const trackButtonStyle = `
|
||||
#active{
|
||||
background-color: #34eb71;
|
||||
color: #333;
|
||||
}
|
||||
#track-button:hover, #active:hover{
|
||||
background-color: rgba(229, 224, 224, 0.48);
|
||||
}
|
||||
`;
|
||||
|
||||
export default PlaylistView;
|
||||
|
||||
export function TrackTableIndex() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { QIcon, QMouseEvent } from "@nodegui/nodegui";
|
||||
import { CursorShape, QIcon, QKeyEvent, QMouseEvent } from "@nodegui/nodegui";
|
||||
import { LineEdit, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||
import React, { useState } from "react";
|
||||
import { useHistory } from "react-router";
|
||||
@ -6,14 +6,16 @@ import { QueryCacheKeys } from "../conf";
|
||||
import showError from "../helpers/showError";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import { search } from "../icons";
|
||||
import { PlaylistCard } from "./Home";
|
||||
import { TrackButton, TrackTableIndex } from "./PlaylistView";
|
||||
import { TrackTableIndex } from "./PlaylistView";
|
||||
import IconButton from "./shared/IconButton";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import PlaylistCard from "./shared/PlaylistCard";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
|
||||
function Search() {
|
||||
const history = useHistory<{ searchQuery: string }>();
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const { data: searchResults, isError, refetch } = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
||||
const { data: searchResults, refetch, isError, isLoading } = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.search,
|
||||
(spotifyApi) => spotifyApi.search(searchQuery, ["playlist", "track"], { limit: 4 }).then((res) => res.body),
|
||||
{ enabled: false }
|
||||
@ -27,6 +29,7 @@ function Search() {
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = <PlaceholderApplet error={isError} loading={isLoading} message="Failed querying spotify" reload={refetch} />;
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column'; padding: 5px;">
|
||||
<View>
|
||||
@ -37,6 +40,13 @@ function Search() {
|
||||
textChanged(t) {
|
||||
setSearchQuery(t);
|
||||
},
|
||||
KeyRelease(native: any) {
|
||||
const key = new QKeyEvent(native);
|
||||
const isEnter = key.key() === 16777220;
|
||||
if (isEnter) {
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton enabled={searchQuery.length > 0} icon={new QIcon(search)} on={{ clicked: handleSearch }} />
|
||||
@ -44,22 +54,33 @@ function Search() {
|
||||
<ScrollArea style="flex: 1;">
|
||||
<View style="flex-direction: 'column'; flex: 1;">
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text>{`<h2>Songs</h2>`}</Text>
|
||||
<Text
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
on={{
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1 && searchResults?.tracks) {
|
||||
history.push("/search/songs", { searchQuery });
|
||||
}
|
||||
},
|
||||
}}>{`<h2>Songs</h2>`}</Text>
|
||||
<TrackTableIndex />
|
||||
{placeholder}
|
||||
{searchResults?.tracks?.items.map((track, index) => (
|
||||
<TrackButton key={index+track.id} active={false} index={index} track={track} on={{}} onTrackClick={() => {}} />
|
||||
<TrackButton key={index + track.id} index={index} track={track} />
|
||||
))}
|
||||
</View>
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
on={{
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1) {
|
||||
if (new QMouseEvent(native).button() === 1 && searchResults?.playlists) {
|
||||
history.push("/search/playlists", { searchQuery });
|
||||
}
|
||||
},
|
||||
}}>{`<h2>Playlists</h2>`}</Text>
|
||||
<View style="flex: 1; justify-content: 'space-around'; align-items: 'center';">
|
||||
{placeholder}
|
||||
{searchResults?.playlists?.items.map((playlist, index) => (
|
||||
<PlaylistCard key={index + playlist.id} playlist={playlist} />
|
||||
))}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
@ -6,17 +6,22 @@ import { GenreView } from "./PlaylistGenreView";
|
||||
|
||||
function SearchResultPlaylistCollection() {
|
||||
const location = useLocation<{ searchQuery: string }>();
|
||||
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage } = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.searchPlaylist,
|
||||
(spotifyApi, { pageParam }) => spotifyApi.searchPlaylists(location.state.searchQuery, { limit: 20, offset: pageParam }).then((res) => res.body),
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
return (lastPage.playlists?.offset ?? 0) + 1;
|
||||
if (lastPage.playlists?.next) {
|
||||
return (lastPage.playlists?.offset ?? 0) + (lastPage.playlists?.limit ?? 0);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return (
|
||||
<GenreView
|
||||
isError={isError}
|
||||
isLoading={isLoading || isFetchingNextPage}
|
||||
refetch={refetch}
|
||||
heading={"Search: " + location.state.searchQuery}
|
||||
playlists={
|
||||
(searchResults?.pages
|
||||
@ -24,10 +29,8 @@ function SearchResultPlaylistCollection() {
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[]) ?? []
|
||||
}
|
||||
loadMore={() => {
|
||||
fetchNextPage();
|
||||
}}
|
||||
isLoadable={hasNextPage || !isFetchingNextPage}
|
||||
loadMore={hasNextPage ? () => fetchNextPage() : undefined}
|
||||
isLoadable={!isFetchingNextPage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
58
src/components/SearchResultSongsCollection.tsx
Normal file
58
src/components/SearchResultSongsCollection.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
import { TrackTableIndex } from "./PlaylistView";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
|
||||
function SearchResultSongsCollection() {
|
||||
const location = useLocation<{ searchQuery: string }>();
|
||||
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.searchSongs,
|
||||
(spotifyApi, { pageParam }) => spotifyApi.searchTracks(location.state.searchQuery, { limit: 20, offset: pageParam }).then((res) => res.body),
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.tracks?.next) {
|
||||
return (lastPage.tracks?.offset ?? 0) + (lastPage.tracks?.limit ?? 0);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text>{`
|
||||
<center>
|
||||
<h2>Search: ${location.state.searchQuery}</h2>
|
||||
</center>
|
||||
`}</Text>
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style="flex: 1; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<PlaceholderApplet error={isError} loading={isLoading || isFetchingNextPage} message="Failed querying spotify" reload={refetch} />
|
||||
{searchResults?.pages
|
||||
.map((searchResult) => searchResult.tracks?.items)
|
||||
.filter(Boolean)
|
||||
.flat(1)
|
||||
.map((track, index) => track && <TrackButton key={index + track.id} index={index} track={track} />)}
|
||||
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
style="flex-grow: 0; align-self: 'center';"
|
||||
text="Load more"
|
||||
on={{
|
||||
clicked() {
|
||||
fetchNextPage();
|
||||
},
|
||||
}}
|
||||
enabled={!isFetchingNextPage}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchResultSongsCollection;
|
@ -1,24 +0,0 @@
|
||||
import { View, Button, Text } from "@nodegui/react-nodegui";
|
||||
import React from "react"
|
||||
|
||||
interface ErrorAppletProps {
|
||||
message?: string;
|
||||
reload: Function;
|
||||
helps?: boolean;
|
||||
}
|
||||
|
||||
function ErrorApplet({ message, reload, helps }: ErrorAppletProps) {
|
||||
return (
|
||||
<View style="flex-direction: 'column'; align-items: 'center';">
|
||||
<Text openExternalLinks>{`
|
||||
<h3>${message ? message : 'An error occured'}</h3>
|
||||
${helps ? `<p>Check if you're connected to internet & then try again. If the issue still persists ask for help or create a <a href="https://github.com/krtirtho/spotube/issues">issue</a>
|
||||
</p>`: ``
|
||||
}
|
||||
`}</Text>
|
||||
<Button on={{ clicked() { reload() } }} text="Reload" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorApplet;
|
56
src/components/shared/PlaceholderApplet.tsx
Normal file
56
src/components/shared/PlaceholderApplet.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { View, Button, Text } from "@nodegui/react-nodegui";
|
||||
import { QLabel, QMovie, } from "@nodegui/nodegui";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { loadingSpinner } from "../../icons";
|
||||
|
||||
interface ErrorAppletProps {
|
||||
error: boolean;
|
||||
loading: boolean;
|
||||
message?: string;
|
||||
reload: Function;
|
||||
helps?: boolean;
|
||||
}
|
||||
|
||||
function PlaceholderApplet({ message, reload, helps, loading, error }: ErrorAppletProps) {
|
||||
const textRef = useRef<QLabel>();
|
||||
const movie = new QMovie();
|
||||
|
||||
useEffect(() => {
|
||||
movie.setFileName(loadingSpinner);
|
||||
textRef.current?.setMovie(movie);
|
||||
textRef.current?.show();
|
||||
movie.start();
|
||||
}, []);
|
||||
if (loading) {
|
||||
return (
|
||||
<View style="flex: 1; justify-content: 'center'; align-items: 'center';">
|
||||
<Text ref={textRef} />
|
||||
</View>
|
||||
);
|
||||
} else if (error) {
|
||||
return (
|
||||
<View style="flex-direction: 'column'; align-items: 'center';">
|
||||
<Text openExternalLinks>{`
|
||||
<h3>${message ? message : "An error occured"}</h3>
|
||||
${
|
||||
helps
|
||||
? `<p>Check if you're connected to internet & then try again. If the issue still persists ask for help or create a <a href="https://github.com/krtirtho/spotube/issues">issue</a>
|
||||
</p>`
|
||||
: ``
|
||||
}
|
||||
`}</Text>
|
||||
<Button
|
||||
on={{
|
||||
clicked() {
|
||||
reload();
|
||||
},
|
||||
}}
|
||||
text="Reload"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default PlaceholderApplet;
|
123
src/components/shared/PlaylistCard.tsx
Normal file
123
src/components/shared/PlaylistCard.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { CursorShape, QIcon, QMouseEvent } from '@nodegui/nodegui';
|
||||
import { Text, View } from '@nodegui/react-nodegui';
|
||||
import React, { useContext, useMemo, useState } from 'react'
|
||||
import { useHistory } from 'react-router';
|
||||
import { QueryCacheKeys } from '../../conf';
|
||||
import playerContext from '../../context/playerContext';
|
||||
import { generateRandomColor, getDarkenForeground } from '../../helpers/RandomColor';
|
||||
import showError from '../../helpers/showError';
|
||||
import usePlaylistReaction from '../../hooks/usePlaylistReaction';
|
||||
import useSpotifyQuery from '../../hooks/useSpotifyQuery';
|
||||
import { heart, heartRegular, pause, play } from '../../icons';
|
||||
import { audioPlayer } from '../Player';
|
||||
import IconButton from './IconButton';
|
||||
|
||||
interface PlaylistCardProps {
|
||||
playlist: SpotifyApi.PlaylistObjectSimplified;
|
||||
}
|
||||
|
||||
const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
|
||||
const { id, description, name, images } = playlist;
|
||||
const history = useHistory();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } = useContext(playerContext);
|
||||
const { refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>([QueryCacheKeys.playlistTracks, id], (spotifyApi) => spotifyApi.getPlaylistTracks(id).then((track) => track.body.items), {
|
||||
initialData: [],
|
||||
enabled: false,
|
||||
});
|
||||
const { reactToPlaylist, isFavorite } = usePlaylistReaction();
|
||||
|
||||
const handlePlaylistPlayPause = async () => {
|
||||
try {
|
||||
const { data: tracks, isSuccess } = await refetch();
|
||||
if (currentPlaylist?.id !== id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ tracks, id, name, thumbnail: images[0].url });
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
await audioPlayer.stop();
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed adding playlist to queue]: ");
|
||||
}
|
||||
};
|
||||
|
||||
function gotoPlaylist(native?: any) {
|
||||
const key = new QMouseEvent(native);
|
||||
if (key.button() === 1) {
|
||||
history.push(`/playlist/${id}`, { name, thumbnail: images[0].url });
|
||||
}
|
||||
}
|
||||
|
||||
const bgColor1 = useMemo(() => generateRandomColor(), []);
|
||||
const color = useMemo(() => getDarkenForeground(bgColor1), [bgColor1]);
|
||||
|
||||
const playlistStyleSheet = `
|
||||
#playlist-container{
|
||||
width: 150px;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
min-height: 150px;
|
||||
background-color: ${bgColor1};
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
#playlist-container:hover{
|
||||
border: 1px solid green;
|
||||
}
|
||||
#playlist-container:clicked{
|
||||
border: 5px solid green;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<View
|
||||
id="playlist-container"
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
styleSheet={playlistStyleSheet}
|
||||
on={{
|
||||
MouseButtonRelease: gotoPlaylist,
|
||||
HoverEnter() {
|
||||
setHovered(true);
|
||||
},
|
||||
HoverLeave() {
|
||||
setHovered(false);
|
||||
},
|
||||
}}>
|
||||
{/* <CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} /> */}
|
||||
<Text style={`color: ${color};`} wordWrap on={{ MouseButtonRelease: gotoPlaylist }}>
|
||||
{`
|
||||
<center>
|
||||
<h3>${name}</h3>
|
||||
<p>${description}</p>
|
||||
</center>
|
||||
`}
|
||||
</Text>
|
||||
{(hovered || currentPlaylist?.id === id) && (
|
||||
<>
|
||||
<IconButton
|
||||
style={`position: absolute; bottom: 30px; left: '55%'; background-color: ${color};`}
|
||||
icon={new QIcon(isFavorite(id) ? heart : heartRegular)}
|
||||
on={{
|
||||
clicked() {
|
||||
reactToPlaylist(playlist);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={new QIcon(currentPlaylist?.id === id ? pause : play)}
|
||||
style={`position: absolute; bottom: 30px; left: '80%'; background-color: ${color};`}
|
||||
on={{
|
||||
clicked() {
|
||||
handlePlaylistPlayPause();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaylistCard;
|
85
src/components/shared/TrackButton.tsx
Normal file
85
src/components/shared/TrackButton.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { QIcon, QMouseEvent } from "@nodegui/nodegui";
|
||||
import { Text, View } from "@nodegui/react-nodegui";
|
||||
import React, { FC, useContext, useMemo } from "react";
|
||||
import playerContext from "../../context/playerContext";
|
||||
import { msToMinAndSec } from "../../helpers/msToMin:sec";
|
||||
import useTrackReaction from "../../hooks/useTrackReaction";
|
||||
import { heart, heartRegular, pause, play } from "../../icons";
|
||||
import { audioPlayer } from "../Player";
|
||||
import IconButton from "./IconButton";
|
||||
|
||||
export interface TrackButtonProps {
|
||||
track: SpotifyApi.TrackObjectFull;
|
||||
playlist?: SpotifyApi.PlaylistObjectFull;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const TrackButton: FC<TrackButtonProps> = ({ track, index, playlist }) => {
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
|
||||
const handlePlaylistPlayPause = (index?: number) => {
|
||||
if (currentPlaylist?.id !== playlist?.id && playlist?.tracks) {
|
||||
setCurrentPlaylist({ id: playlist.id, name: playlist.name, thumbnail: playlist.images[0].url, tracks: playlist.tracks.items });
|
||||
setCurrentTrack(playlist.tracks.items[index ?? 0].track);
|
||||
} else {
|
||||
audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error));
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const trackClickHandler = (track: SpotifyApi.TrackObjectFull) => {
|
||||
setCurrentTrack(track);
|
||||
};
|
||||
|
||||
const duration = useMemo(() => msToMinAndSec(track.duration_ms), []);
|
||||
const active = (currentPlaylist?.id === playlist?.id && currentTrack?.id === track.id) || currentTrack?.id === track.id;
|
||||
return (
|
||||
<View
|
||||
id={active?"active":"track-button"}
|
||||
styleSheet={trackButtonStyle}
|
||||
on={{
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1 && playlist) {
|
||||
handlePlaylistPlayPause(index);
|
||||
}
|
||||
},
|
||||
}}>
|
||||
<Text style="padding: 5px;">{index + 1}</Text>
|
||||
<View style="flex-direction: 'column'; width: '35%';">
|
||||
<Text>{`<h3>${track.name}</h3>`}</Text>
|
||||
<Text>{track.artists.map((artist) => artist.name).join(", ")}</Text>
|
||||
</View>
|
||||
<Text style="width: '25%';">{track.album.name}</Text>
|
||||
<Text style="width: '15%';">{duration}</Text>
|
||||
<View style="width: '15%'; padding: 5px; justify-content: 'space-around';">
|
||||
<IconButton
|
||||
icon={new QIcon(isFavorite(track.id) ? heart : heartRegular)}
|
||||
on={{
|
||||
clicked() {
|
||||
reactToTrack({ track, added_at: "" });
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={new QIcon(active ? pause : play)}
|
||||
on={{
|
||||
clicked() {
|
||||
trackClickHandler(track);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const trackButtonStyle = `
|
||||
#active{
|
||||
background-color: #34eb71;
|
||||
color: #333;
|
||||
}
|
||||
#track-button:hover{
|
||||
background-color: rgba(229, 224, 224, 0.48);
|
||||
}
|
||||
`;
|
@ -17,5 +17,6 @@ export enum QueryCacheKeys{
|
||||
userPlaylists = "user-palylists",
|
||||
userSavedTracks = "user-saved-tracks",
|
||||
search = "search",
|
||||
searchPlaylist="searchPlaylist"
|
||||
searchPlaylist = "searchPlaylist",
|
||||
searchSongs = "searchSongs"
|
||||
}
|
@ -1,13 +1,18 @@
|
||||
import { useQueryClient } from "react-query";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyInfiniteQuery from "./useSpotifyInfiniteQuery";
|
||||
import useSpotifyMutation from "./useSpotifyMutation";
|
||||
import useSpotifyQuery from "./useSpotifyQuery";
|
||||
|
||||
function useTrackReaction() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: favoriteTracks } = useSpotifyQuery<SpotifyApi.SavedTrackObject[]>(QueryCacheKeys.userSavedTracks, (spotifyApi) =>
|
||||
spotifyApi.getMySavedTracks({ limit: 50 }).then((tracks) => tracks.body.items)
|
||||
const { data: userSavedTracks } = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(QueryCacheKeys.userSavedTracks, (spotifyApi, { pageParam }) =>
|
||||
spotifyApi.getMySavedTracks({ limit: 50, offset: pageParam }).then((res) => res.body)
|
||||
);
|
||||
const favoriteTracks = userSavedTracks?.pages
|
||||
?.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
||||
const { mutate: reactToTrack } = useSpotifyMutation<{}, SpotifyApi.SavedTrackObject>(
|
||||
(spotifyApi, { track }) => spotifyApi[isFavorite(track.id) ? "removeFromMySavedTracks" : "addToMySavedTracks"]([track.id]).then((res) => res.body),
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ import _heart from "../assets/heart-solid.svg"
|
||||
import _random from "../assets/random-solid.svg"
|
||||
import _stop from "../assets/stop-solid.svg"
|
||||
import _search from "../assets/search-solid.svg";
|
||||
import _loadingSpinner from "../assets/loading-spinner.gif";
|
||||
|
||||
export const play = _play;
|
||||
export const pause = _pause;
|
||||
@ -19,3 +20,4 @@ export const heart = _heart;
|
||||
export const random = _random;
|
||||
export const stop = _stop;
|
||||
export const search = _search;
|
||||
export const loadingSpinner = _loadingSpinner;
|
@ -10,6 +10,7 @@ import CurrentPlaylist from "./components/CurrentPlaylist";
|
||||
import Library from "./components/Library";
|
||||
import Search from "./components/Search";
|
||||
import SearchResultPlaylistCollection from "./components/SearchResultPlaylistCollection";
|
||||
import SearchResultSongsCollection from "./components/SearchResultSongsCollection";
|
||||
|
||||
function Routes() {
|
||||
const { isLoggedIn } = useContext(authContext);
|
||||
@ -46,6 +47,9 @@ function Routes() {
|
||||
<Route exact path="/search/playlists">
|
||||
<SearchResultPlaylistCollection />
|
||||
</Route>
|
||||
<Route exact path="/search/songs">
|
||||
<SearchResultSongsCollection />
|
||||
</Route>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user