mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Search & SearchResultPlaylist see all added. Spotify Infinite Query added for infinite scroll
This commit is contained in:
parent
be612610d9
commit
d695172804
@ -75,9 +75,12 @@ const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
|||||||
{ initialData: [] }
|
{ initialData: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
function goToGenre() {
|
function goToGenre(native: any) {
|
||||||
|
const mouse = new QMouseEvent(native);
|
||||||
|
if (mouse.button() === 1) {
|
||||||
history.push(`/genre/playlists/${id}`, { name });
|
history.push(`/genre/playlists/${id}`, { name });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Direction } from "@nodegui/nodegui";
|
import { QAbstractButtonSignals } from "@nodegui/nodegui";
|
||||||
import { BoxView, ScrollArea, Text, View, GridView, GridColumn, GridRow } from "@nodegui/react-nodegui";
|
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useLocation, useParams } from "react-router";
|
import { useLocation, useParams } from "react-router";
|
||||||
import { QueryCacheKeys } from "../conf";
|
import { QueryCacheKeys } from "../conf";
|
||||||
@ -12,11 +12,23 @@ function PlaylistGenreView() {
|
|||||||
const location = useLocation<{ name: string }>();
|
const location = useLocation<{ name: string }>();
|
||||||
const { data: playlists } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
const { data: playlists } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
||||||
[QueryCacheKeys.genrePlaylists, id],
|
[QueryCacheKeys.genrePlaylists, id],
|
||||||
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id)
|
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id).then((playlistsRes) => playlistsRes.body.playlists.items),
|
||||||
.then((playlistsRes) => playlistsRes.body.playlists.items),
|
|
||||||
{ initialData: [] }
|
{ initialData: [] }
|
||||||
)
|
);
|
||||||
|
|
||||||
|
return <GenreView heading={location.state.name} playlists={playlists ?? []} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlaylistGenreView;
|
||||||
|
|
||||||
|
interface GenreViewProps {
|
||||||
|
heading: string;
|
||||||
|
playlists: SpotifyApi.PlaylistObjectSimplified[];
|
||||||
|
loadMore?: QAbstractButtonSignals["clicked"];
|
||||||
|
isLoadable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenreView({ heading, playlists, loadMore, isLoadable }: GenreViewProps) {
|
||||||
const playlistGenreViewStylesheet = `
|
const playlistGenreViewStylesheet = `
|
||||||
#genre-container{
|
#genre-container{
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -38,25 +50,18 @@ function PlaylistGenreView() {
|
|||||||
width: 330px;
|
width: 330px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View id="genre-container" styleSheet={playlistGenreViewStylesheet}>
|
<View id="genre-container" styleSheet={playlistGenreViewStylesheet}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<Text id="heading">{`<h2>${location.state.name}</h2>`}</Text>
|
<Text id="heading">{`<h2>${heading}</h2>`}</Text>
|
||||||
<ScrollArea id="scroll-view">
|
<ScrollArea id="scroll-view">
|
||||||
<View id="child-container">
|
<View id="child-container">
|
||||||
{playlists?.map((playlist, index) => {
|
{playlists?.map((playlist, index) => {
|
||||||
return (
|
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
||||||
<PlaylistCard
|
|
||||||
key={index + playlist.id}
|
|
||||||
playlist={playlist}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
|
{loadMore && <Button text="Load more" on={{ clicked: loadMore }} enabled={isLoadable} />}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlaylistGenreView;
|
|
||||||
|
@ -76,6 +76,7 @@ const PlaylistView: FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{tracks?.map(({ track }, index) => {
|
{tracks?.map(({ track }, index) => {
|
||||||
|
if (track) {
|
||||||
return (
|
return (
|
||||||
<TrackButton
|
<TrackButton
|
||||||
key={index + track.id}
|
key={index + track.id}
|
||||||
@ -90,6 +91,7 @@ const PlaylistView: FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { QIcon } from "@nodegui/nodegui";
|
import { QIcon, QMouseEvent } from "@nodegui/nodegui";
|
||||||
import { LineEdit, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
import { LineEdit, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useHistory } from "react-router";
|
||||||
import { QueryCacheKeys } from "../conf";
|
import { QueryCacheKeys } from "../conf";
|
||||||
import showError from "../helpers/showError";
|
import showError from "../helpers/showError";
|
||||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||||
@ -10,10 +11,11 @@ import { TrackButton, TrackTableIndex } from "./PlaylistView";
|
|||||||
import IconButton from "./shared/IconButton";
|
import IconButton from "./shared/IconButton";
|
||||||
|
|
||||||
function Search() {
|
function Search() {
|
||||||
|
const history = useHistory<{ searchQuery: string }>();
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const { data: searchResults, isError, refetch } = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
const { data: searchResults, isError, refetch } = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
||||||
QueryCacheKeys.search,
|
QueryCacheKeys.search,
|
||||||
(spotifyApi) => spotifyApi.search(searchQuery, ["playlist", "track"]).then((res) => res.body),
|
(spotifyApi) => spotifyApi.search(searchQuery, ["playlist", "track"], { limit: 4 }).then((res) => res.body),
|
||||||
{ enabled: false }
|
{ enabled: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -45,18 +47,23 @@ function Search() {
|
|||||||
<Text>{`<h2>Songs</h2>`}</Text>
|
<Text>{`<h2>Songs</h2>`}</Text>
|
||||||
<TrackTableIndex />
|
<TrackTableIndex />
|
||||||
{searchResults?.tracks?.items.map((track, index) => (
|
{searchResults?.tracks?.items.map((track, index) => (
|
||||||
<TrackButton active={false} index={index} track={track} on={{}} onTrackClick={() => {}} />
|
<TrackButton key={index+track.id} active={false} index={index} track={track} on={{}} onTrackClick={() => {}} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style="flex: 1; flex-direction: 'column';">
|
<View style="flex: 1; flex-direction: 'column';">
|
||||||
<Text>{`<h2>Playlists</h2>`}</Text>
|
<Text
|
||||||
<ScrollArea style="flex: 1;">
|
on={{
|
||||||
<View style="flex-direction: 'row'; align-items: 'center'; flex-wrap: 'wrap'; width: 330px;">
|
MouseButtonRelease(native: any) {
|
||||||
{searchResults?.playlists?.items.map((playlist) => (
|
if (new QMouseEvent(native).button() === 1) {
|
||||||
<PlaylistCard playlist={playlist} />
|
history.push("/search/playlists", { searchQuery });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}>{`<h2>Playlists</h2>`}</Text>
|
||||||
|
<View style="flex: 1; justify-content: 'space-around'; align-items: 'center';">
|
||||||
|
{searchResults?.playlists?.items.map((playlist, index) => (
|
||||||
|
<PlaylistCard key={index+playlist.id} playlist={playlist} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
35
src/components/SearchResultPlaylistCollection.tsx
Normal file
35
src/components/SearchResultPlaylistCollection.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
import { QueryCacheKeys } from "../conf";
|
||||||
|
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||||
|
import { GenreView } from "./PlaylistGenreView";
|
||||||
|
|
||||||
|
function SearchResultPlaylistCollection() {
|
||||||
|
const location = useLocation<{ searchQuery: string }>();
|
||||||
|
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage } = 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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<GenreView
|
||||||
|
heading={"Search: "+location.state.searchQuery}
|
||||||
|
playlists={
|
||||||
|
(searchResults?.pages
|
||||||
|
?.map((page) => page.playlists?.items)
|
||||||
|
.filter(Boolean)
|
||||||
|
.flat(1) as SpotifyApi.PlaylistObjectSimplified[]) ?? []
|
||||||
|
}
|
||||||
|
loadMore={() => {
|
||||||
|
fetchNextPage();
|
||||||
|
}}
|
||||||
|
isLoadable={hasNextPage || !isFetchingNextPage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchResultPlaylistCollection;
|
@ -16,5 +16,6 @@ export enum QueryCacheKeys{
|
|||||||
playlistTracks="playlistTracks",
|
playlistTracks="playlistTracks",
|
||||||
userPlaylists = "user-palylists",
|
userPlaylists = "user-palylists",
|
||||||
userSavedTracks = "user-saved-tracks",
|
userSavedTracks = "user-saved-tracks",
|
||||||
search="search"
|
search = "search",
|
||||||
|
searchPlaylist="searchPlaylist"
|
||||||
}
|
}
|
@ -17,7 +17,6 @@ function useSpotifyApi() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn && clientId && clientSecret && refreshToken) {
|
if (isLoggedIn && clientId && clientSecret && refreshToken) {
|
||||||
console.log(chalk.bgCyan.black("Setting up spotify credentials"));
|
|
||||||
spotifyApi.setClientId(clientId);
|
spotifyApi.setClientId(clientId);
|
||||||
spotifyApi.setClientSecret(clientSecret);
|
spotifyApi.setClientSecret(clientSecret);
|
||||||
spotifyApi.setRefreshToken(refreshToken);
|
spotifyApi.setRefreshToken(refreshToken);
|
||||||
@ -25,7 +24,6 @@ function useSpotifyApi() {
|
|||||||
spotifyApi
|
spotifyApi
|
||||||
.refreshAccessToken()
|
.refreshAccessToken()
|
||||||
.then((token) => {
|
.then((token) => {
|
||||||
console.log(chalk.bgRedBright.yellow("Refreshing access token from useSpotifyApi"));
|
|
||||||
setAccess_token(token.body.access_token);
|
setAccess_token(token.body.access_token);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -33,7 +31,6 @@ function useSpotifyApi() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
spotifyApi.setAccessToken(access_token);
|
spotifyApi.setAccessToken(access_token);
|
||||||
console.log(chalk.bgCyan.green("Finished setting up credentials"));
|
|
||||||
}
|
}
|
||||||
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
||||||
|
|
||||||
|
28
src/hooks/useSpotifyInfiniteQuery.ts
Normal file
28
src/hooks/useSpotifyInfiniteQuery.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { QueryFunctionContext, QueryKey, useInfiniteQuery, UseInfiniteQueryOptions, UseInfiniteQueryResult } from "react-query";
|
||||||
|
import SpotifyWebApi from "spotify-web-api-node";
|
||||||
|
import useSpotifyApi from "./useSpotifyApi";
|
||||||
|
import useSpotifyApiError from "./useSpotifyApiError";
|
||||||
|
|
||||||
|
type SpotifyQueryFn<TQueryData> = (spotifyApi: SpotifyWebApi, pageArgs: QueryFunctionContext) => Promise<TQueryData>;
|
||||||
|
|
||||||
|
function useSpotifyInfiniteQuery<TQueryData = unknown>(
|
||||||
|
queryKey: QueryKey,
|
||||||
|
queryHandler: SpotifyQueryFn<TQueryData>,
|
||||||
|
options: UseInfiniteQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {}
|
||||||
|
): UseInfiniteQueryResult<TQueryData, SpotifyApi.ErrorObject> {
|
||||||
|
const spotifyApi = useSpotifyApi();
|
||||||
|
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||||
|
const query = useInfiniteQuery<TQueryData, SpotifyApi.ErrorObject>(queryKey, (pageArgs) => queryHandler(spotifyApi, pageArgs), options);
|
||||||
|
const { isError, error } = query;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isError && error) {
|
||||||
|
handleSpotifyError(error);
|
||||||
|
}
|
||||||
|
}, [isError, error]);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSpotifyInfiniteQuery;
|
@ -9,6 +9,7 @@ import TabMenu from "./components/TabMenu";
|
|||||||
import CurrentPlaylist from "./components/CurrentPlaylist";
|
import CurrentPlaylist from "./components/CurrentPlaylist";
|
||||||
import Library from "./components/Library";
|
import Library from "./components/Library";
|
||||||
import Search from "./components/Search";
|
import Search from "./components/Search";
|
||||||
|
import SearchResultPlaylistCollection from "./components/SearchResultPlaylistCollection";
|
||||||
|
|
||||||
function Routes() {
|
function Routes() {
|
||||||
const { isLoggedIn } = useContext(authContext);
|
const { isLoggedIn } = useContext(authContext);
|
||||||
@ -33,13 +34,18 @@ function Routes() {
|
|||||||
<Login />
|
<Login />
|
||||||
)}
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/search"><Search/></Route>
|
|
||||||
<Route path="/currently">
|
<Route path="/currently">
|
||||||
<CurrentPlaylist />
|
<CurrentPlaylist />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/library">
|
<Route path="/library">
|
||||||
<Library />
|
<Library />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/search">
|
||||||
|
<Search />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/search/playlists">
|
||||||
|
<SearchResultPlaylistCollection />
|
||||||
|
</Route>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2016",
|
"target": "es2016",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
|
"lib": [
|
||||||
|
"ES2019.Array",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
|
Loading…
Reference in New Issue
Block a user