follow & unfollow playlist feature, colorful playlistButton for lessening resource usage, was added

This commit is contained in:
krtirtho 2021-03-15 17:41:17 +06:00
parent 9751b4f837
commit 9df74fbe36
16 changed files with 377 additions and 7953 deletions

View File

@ -4,5 +4,5 @@ Name=Spotube
Exec=AppRun
Icon=default
Comment=A music streaming app combining the power of Spotify & Youtube
Terminal=true
Terminal=false
Categories=Music;

7571
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
"axios": "^0.21.1",
"base64url": "^3.0.1",
"chalk": "^4.1.0",
"color": "^3.1.3",
"dotenv": "^8.2.0",
"du": "^1.0.0",
"express": "^4.17.1",
@ -40,6 +41,7 @@
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@nodegui/packer": "^1.4.1",
"@types/color": "^3.0.1",
"@types/du": "^1.0.0",
"@types/express": "^4.17.11",
"@types/is-url": "^1.2.28",

View File

@ -1,327 +0,0 @@
{
"auto_complete":
{
"selected_items":
[
]
},
"buffers":
[
{
"file": "src/components/Player.tsx",
"settings":
{
"buffer_size": 8355,
"line_ending": "Unix"
}
},
{
"file": "src/routes.tsx",
"settings":
{
"buffer_size": 688,
"line_ending": "Unix"
}
}
],
"build_system": "",
"build_system_choices":
[
],
"build_varint": "",
"command_palette":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
[
"Types: Sh",
"TypeScript: Show Error List"
],
[
"pro",
"Project: Save As"
],
[
"insta",
"Package Control: Install Package"
],
[
"re",
"Package Control: Remove Package"
],
[
"lsp type",
"Preferences: LSP-typescript Settings"
],
[
"install ",
"Package Control: Install Package"
],
[
"LSP: enale",
"LSP: Enable Language Server in Project"
],
[
"remove",
"Package Control: Remove Package"
],
[
"theme",
"UI: Select Theme"
],
[
"termi",
"Terminus: Close"
],
[
"Sync Up",
"Sync Settings: Upload"
],
[
"Sync crea",
"Sync Settings: Create and Upload"
],
[
"package ins",
"Package Control: Install Package"
],
[
"pac",
"Install Package Control"
]
],
"width": 0.0
},
"console":
{
"height": 170.0,
"history":
[
]
},
"distraction_free":
{
"menu_visible": true,
"show_minimap": false,
"show_open_files": false,
"show_tabs": false,
"side_bar_visible": false,
"status_bar_visible": false
},
"file_history":
[
"/home/krtirtho/development/desktop-dev/spotube/spotube.sublime-project",
"/home/krtirtho/.config/sublime-text-3/Packages/User/Preferences.sublime-settings",
"/home/krtirtho/.config/sublime-text-3/Packages/TypeScript/Preferences.sublime-settings",
"/home/krtirtho/.config/sublime-text-3/Packages/TypeScript/TypeScriptReact.sublime-settings",
"/home/krtirtho/.config/sublime-text-3/Packages/TypeScript/TypeScript.sublime-settings",
"/home/krtirtho/.config/sublime-text-3/Packages/Default/Default (Linux).sublime-keymap",
"/home/krtirtho/.config/sublime-text-3/Packages/User/Default (Linux).sublime-keymap",
"/home/krtirtho/.config/sublime-text-3/Packages/Sync Settings/SyncSettings.sublime-settings",
"/home/krtirtho/.config/sublime-text-3/Packages/User/SyncSettings.sublime-settings"
],
"find":
{
"height": 26.0
},
"find_in_files":
{
"height": 0.0,
"where_history":
[
]
},
"find_state":
{
"case_sensitive": false,
"find_history":
[
],
"highlight": true,
"in_selection": false,
"preserve_case": false,
"regex": false,
"replace_history":
[
],
"reverse": false,
"show_context": true,
"use_buffer2": true,
"whole_word": false,
"wrap": true
},
"groups":
[
{
"selected": 1,
"sheets":
[
{
"buffer": 0,
"file": "src/components/Player.tsx",
"semi_transient": false,
"settings":
{
"buffer_size": 8355,
"regions":
{
},
"selection":
[
[
2966,
2966
]
],
"settings":
{
"syntax": "Packages/TypeScript/TypeScriptReact.tmLanguage",
"tab_size": 2,
"translate_tabs_to_spaces": true,
"use_tab_stops": false
},
"translation.x": 0.0,
"translation.y": 1275.0,
"zoom_level": 1.0
},
"stack_index": 1,
"type": "text"
},
{
"buffer": 1,
"file": "src/routes.tsx",
"semi_transient": false,
"settings":
{
"buffer_size": 688,
"regions":
{
},
"selection":
[
[
688,
688
]
],
"settings":
{
"color_scheme": "Packages/GitHub Theme/schemes/GitHub Dark.sublime-color-scheme",
"syntax": "Packages/TypeScript/TypeScriptReact.tmLanguage",
"tab_size": 2,
"translate_tabs_to_spaces": true,
"typescript_plugin_format_options":
{
"convertTabsToSpaces": true,
"indentSize": 2,
"tabSize": 2
},
"use_tab_stops": false
},
"translation.x": 0.0,
"translation.y": 0.0,
"zoom_level": 1.0
},
"stack_index": 0,
"type": "text"
}
]
}
],
"incremental_find":
{
"height": 26.0
},
"input":
{
"height": 38.0
},
"layout":
{
"cells":
[
[
0,
0,
1,
1
]
],
"cols":
[
0.0,
1.0
],
"rows":
[
0.0,
1.0
]
},
"menu_visible": true,
"output.diagnostics":
{
"height": 0.0
},
"output.doc":
{
"height": 0.0
},
"output.errorlist":
{
"height": 140.0
},
"output.find_results":
{
"height": 0.0
},
"pinned_build_system": "",
"project": "spotube.sublime-project",
"replace":
{
"height": 48.0
},
"save_all_on_build": true,
"select_file":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
],
"width": 0.0
},
"select_project":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
],
"width": 0.0
},
"select_symbol":
{
"height": 0.0,
"last_filter": "",
"selected_items":
[
],
"width": 0.0
},
"selected_group": 0,
"settings":
{
},
"show_minimap": true,
"show_open_files": true,
"show_tabs": true,
"side_bar_visible": true,
"side_bar_width": 225.0,
"status_bar_visible": true,
"template_settings":
{
}
}

View File

@ -122,7 +122,7 @@ function RootApp() {
console.log("Server is running");
spotifyApi.setClientId(credentials.clientId);
spotifyApi.setClientSecret(credentials.clientSecret);
open(spotifyApi.createAuthorizeURL(["user-library-read", "playlist-read-private", "user-library-modify"], "xxxyyysssddd")).catch((e) =>
open(spotifyApi.createAuthorizeURL(["user-library-read", "playlist-read-private", "user-library-modify","playlist-modify-private", "playlist-modify-public"], "xxxyyysssddd")).catch((e) =>
console.error("Opening IPC connection with browser failed: ", e)
);
});

View File

@ -1,7 +1,7 @@
import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
import React, { useContext } from "react";
import playerContext from "../context/playerContext";
import { TrackButton } from "./PlaylistView";
import { TrackButton, TrackTableIndex } from "./PlaylistView";
function CurrentPlaylist() {
const { currentPlaylist, currentTrack, setCurrentTrack } = useContext(playerContext);
@ -12,7 +12,8 @@ function CurrentPlaylist() {
return (
<View style="flex: 1; flex-direction: 'column';">
<Text>{ `<center><h2>${currentPlaylist?.name}</h2></center>` }</Text>
<Text>{`<center><h2>${currentPlaylist?.name}</h2></center>`}</Text>
<TrackTableIndex />
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
<View style={`flex-direction:column; flex: 1;`}>
{currentPlaylist?.tracks.map(({ track }, index) => {
@ -20,9 +21,10 @@ function CurrentPlaylist() {
<TrackButton
key={index + track.id}
active={currentTrack?.id === track.id}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
on={{ clicked: () => setCurrentTrack(track) }}
track={track}
index={index}
on={{ MouseButtonRelease: () => setCurrentTrack(track) }}
onTrackClick={() => {}}
/>
);
})}

View File

@ -1,11 +1,18 @@
import React from "react";
import { Button, ScrollArea, BoxView, View } from "@nodegui/react-nodegui";
import React, { useContext, useMemo, useState } from "react";
import { Button, ScrollArea, View, Text } from "@nodegui/react-nodegui";
import { useHistory } from "react-router";
import CachedImage from "./shared/CachedImage";
import { CursorShape, Direction } from "@nodegui/nodegui";
import { CursorShape, QIcon, 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";
function Home() {
const { data: categories, isError, isRefetchError, refetch } = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
@ -33,32 +40,6 @@ interface CategoryCardProps {
name: string;
}
const CategoryCard = ({ id, name }: CategoryCardProps) => {
const history = useHistory();
const { data: playlists, isError } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
[QueryCacheKeys.categoryPlaylists, id],
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id, { limit: 4 }).then((playlistsRes) => playlistsRes.body.playlists.items),
{ initialData: [] }
);
function goToGenre() {
history.push(`/genre/playlists/${id}`, { name });
}
if (isError) {
return <></>;
}
return (
<View id="container" styleSheet={categoryStylesheet}>
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
<View id="child-view">
{playlists?.map((playlist, index) => {
return <PlaylistCard key={index + playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
})}
</View>
</View>
);
};
const categoryStylesheet = `
#container{
flex: 1;
@ -86,25 +67,81 @@ const categoryStylesheet = `
text-decoration: underline;
}
`;
const CategoryCard = ({ id, name }: CategoryCardProps) => {
const history = useHistory();
const { data: playlists, isError } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
[QueryCacheKeys.categoryPlaylists, id],
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id, { limit: 4 }).then((playlistsRes) => playlistsRes.body.playlists.items),
{ initialData: [] }
);
function goToGenre() {
history.push(`/genre/playlists/${id}`, { name });
}
if (isError) {
return <></>;
}
return (
<View id="container" styleSheet={categoryStylesheet}>
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
<View id="child-view">
{playlists?.map((playlist, index) => {
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
})}
</View>
</View>
);
};
interface PlaylistCardProps {
thumbnail: string;
name: string;
id: string;
playlist: SpotifyApi.PlaylistObjectSimplified;
}
export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardProps) => {
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();
function gotoPlaylist() {
history.push(`/playlist/${id}`, { name, thumbnail });
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{
max-width: 250px;
width: 150px;
flex-direction: column;
padding: 2px;
padding: 5px;
min-height: 150px;
background-color: ${bgColor1};
border-radius: 5px;
}
#playlist-container:hover{
border: 1px solid green;
@ -115,8 +152,50 @@ export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardPro
`;
return (
<View id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}>
<CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />
<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>
);
});

View File

@ -5,7 +5,7 @@ import { QueryCacheKeys } from "../conf";
import playerContext from "../context/playerContext";
import useSpotifyQuery from "../hooks/useSpotifyQuery";
import { PlaylistCard } from "./Home";
import { PlaylistSimpleControls, TrackButton } from "./PlaylistView";
import { PlaylistSimpleControls, TrackButton, TrackTableIndex } from "./PlaylistView";
import { TabMenuItem } from "./TabMenu";
function Library() {
@ -39,7 +39,7 @@ function UserPlaylists() {
<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';">
{userPlaylists?.map((playlist, index) => (
<PlaylistCard key={index + playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />
<PlaylistCard key={index + playlist.id} playlist={playlist} />
))}
</View>
</ScrollArea>
@ -53,10 +53,10 @@ function UserSavedTracks() {
);
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
function handlePlaylistPlayPause() {
function handlePlaylistPlayPause(index?: number) {
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
setCurrentPlaylist({ id: userSavedPlaylistId, name: "Liked Tracks", thumbnail: "https://nerdist.com/wp-content/uploads/2020/07/maxresdefault.jpg", tracks: userTracks });
setCurrentTrack(userTracks[0].track);
setCurrentTrack(userTracks[index ?? 0].track);
} else {
setCurrentPlaylist(undefined);
setCurrentTrack(undefined);
@ -66,19 +66,21 @@ function UserSavedTracks() {
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}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
track={track}
index={index}
on={{
clicked() {
MouseButtonRelease() {
setCurrentTrack(track);
},
}}
onTrackClick={()=>handlePlaylistPlayPause(index)}
/>
))}
</View>

View File

@ -26,9 +26,8 @@ export const audioPlayer = new NodeMpv(
function Player(): ReactElement {
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
const { reactToTrack, isFavorite } = useTrackReaction();
const initVolume = parseFloat(localStorage.getItem("volume") ?? "55");
const [isPaused, setIsPaused] = useState(true);
const [volume, setVolume] = useState<number>(initVolume);
const [volume, setVolume] = useState<number>(55);
const [totalDuration, setTotalDuration] = useState(0);
const [shuffle, setShuffle] = useState<boolean>(false);
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
@ -54,10 +53,9 @@ function Player(): ReactElement {
try {
if (!playerRunning) {
await audioPlayer.start();
await audioPlayer.volume(initVolume);
await audioPlayer.volume(volume);
}
} catch (error) {
console.error("Failed to start audio player", error);
showError(error, "[Failed starting audio player]: ");
}
})();
@ -90,6 +88,11 @@ function Player(): ReactElement {
})();
}, [currentTrack]);
// changing shuffle to default
useEffect(() => {
setShuffle(false);
}, [currentPlaylist])
useEffect(() => {
if (playerRunning) {
audioPlayer.volume(volume);
@ -186,7 +189,7 @@ function Player(): ReactElement {
<Text ref={titleRef} wordWrap>
{artistsNames && currentTrack
? `
<p><b>${currentTrack.name}</b> - ${artistsNames[0]} feat. ${artistsNames.slice(1).join(" ")}</p>
<p><b>${currentTrack.name}</b> - ${artistsNames[0]} ${artistsNames.length > 1 ? "feat. " + artistsNames.slice(1).join(", ") : ""}</p>
`
: `<b>Oh, dear don't waste time</b>`}
</Text>

View File

@ -49,9 +49,7 @@ function PlaylistGenreView() {
return (
<PlaylistCard
key={index + playlist.id}
id={playlist.id}
name={playlist.name}
thumbnail={playlist.images[0].url}
playlist={playlist}
/>
);
})}

View File

@ -1,15 +1,18 @@
import React, { FC, useContext } from "react";
import React, { FC, useContext, useMemo } from "react";
import { View, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
import BackButton from "./BackButton";
import { useLocation, useParams } from "react-router";
import { QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
import { QAbstractButtonSignals, QIcon, QMouseEvent, QWidgetSignals } from "@nodegui/nodegui";
import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
import playerContext from "../context/playerContext";
import IconButton from "./shared/IconButton";
import { heartRegular, play, stop } from "../icons";
import { heart, heartRegular, pause, 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";
export interface PlaylistTrackRes {
name: string;
@ -21,17 +24,20 @@ const PlaylistView: FC = () => {
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
const params = useParams<{ id: string }>();
const location = useLocation<{ name: string; thumbnail: string }>();
const { isFavorite, reactToPlaylist } = usePlaylistReaction();
const { data: playlist } = useSpotifyQuery<SpotifyApi.PlaylistObjectFull>([QueryCacheKeys.categoryPlaylists, params.id], (spotifyApi) =>
spotifyApi.getPlaylist(params.id).then((playlistsRes) => playlistsRes.body)
);
const { data: tracks, isSuccess, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
[QueryCacheKeys.playlistTracks, params.id],
(spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
{ initialData: [] }
);
const handlePlaylistPlayPause = () => {
const handlePlaylistPlayPause = (index?: number) => {
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
setCurrentPlaylist({ ...params, ...location.state, tracks });
setCurrentTrack(tracks[0].track);
setCurrentTrack(tracks[index ?? 0].track);
} else {
audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error));
setCurrentTrack(undefined);
@ -45,8 +51,14 @@ const PlaylistView: FC = () => {
return (
<View style={`flex: 1; flex-direction: 'column';`}>
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === params.id} />
<PlaylistSimpleControls
handlePlaylistReact={() => playlist && reactToPlaylist(playlist)}
handlePlaylistPlayPause={handlePlaylistPlayPause}
isActive={currentPlaylist?.id === params.id}
isFavorite={isFavorite(params.id)}
/>
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
{<TrackTableIndex />}
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
<View style={`flex-direction:column; flex: 1;`}>
{isLoading && <Text>{`Loading Tracks...`}</Text>}
@ -67,10 +79,15 @@ const PlaylistView: FC = () => {
return (
<TrackButton
key={index + track.id}
track={track}
index={index}
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
on={{ clicked: () => trackClickHandler(track) }}
on={{
MouseButtonRelease: () => trackClickHandler(track),
}}
onTrackClick={() => {
handlePlaylistPlayPause(index);
}}
/>
);
})}
@ -81,36 +98,95 @@ const PlaylistView: FC = () => {
};
export interface TrackButtonProps {
name: string;
artist: string;
track: SpotifyApi.TrackObjectFull;
on: Partial<QWidgetSignals | WidgetEventListeners>;
onTrackClick?: QAbstractButtonSignals["clicked"];
active: boolean;
on: Partial<QAbstractButtonSignals | WidgetEventListeners>;
index: number;
}
export const TrackButton: FC<TrackButtonProps> = ({ name, artist, on, active }) => {
return <Button id={`${active ? "active" : ""}`} on={on} text={`${name} -- ${artist}`} styleSheet={trackButtonStyle} />;
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: orange;
background-color: #34eb71;
color: #333;
}
#track-button:hover, #active:hover{
background-color: rgba(229, 224, 224, 0.48);
}
`;
export default PlaylistView;
interface PlaylistSimpleControlsProps {
handlePlaylistPlayPause: () => void;
isActive: boolean;
}
export function PlaylistSimpleControls({ handlePlaylistPlayPause, isActive }: PlaylistSimpleControlsProps) {
export function TrackTableIndex() {
return (
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
<BackButton />
<IconButton icon={new QIcon(heartRegular)} />
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(isActive ? stop : play)} />
<View>
<Text style="padding: 5px;">{`<h3>#</h3>`}</Text>
<Text style="width: '35%';">{`<h3>Title</h3>`}</Text>
<Text style="width: '25%';">{`<h3>Album</h3>`}</Text>
<Text style="width: '15%';">{`<h3>Duration</h3>`}</Text>
<Text style="width: '15%';">{`<h3>Actions</h3>`}</Text>
</View>
);
}
interface PlaylistSimpleControlsProps {
handlePlaylistPlayPause: (index?: number) => void;
handlePlaylistReact?: () => void;
isActive: boolean;
isFavorite?: boolean;
}
export function PlaylistSimpleControls({ handlePlaylistPlayPause, isActive, handlePlaylistReact, isFavorite }: PlaylistSimpleControlsProps) {
return (
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
<BackButton />
{isFavorite !== undefined && <IconButton icon={new QIcon(isFavorite ? heart : heartRegular)} on={{ clicked: handlePlaylistReact }} />}
<IconButton
style={`background-color: #00be5f; color: white;`}
on={{
clicked() {
handlePlaylistPlayPause();
},
}}
icon={new QIcon(isActive ? stop : play)}
/>
</View>
);
}

View File

@ -0,0 +1,10 @@
import color from "color";
export function generateRandomColor(lightness: number=70): string {
return "hsl(" + 360 * Math.random() + "," + (25 + 70 * Math.random()) + "%," + (lightness + 10 * Math.random()) + "%)";
}
export function getDarkenForeground(hslcolor: string): string {
const adjustedColor = color(hslcolor);
return adjustedColor.darken(.5).hex();
}

View File

@ -1,35 +0,0 @@
import axios, { AxiosResponse } from "axios";
import qs from "querystring";
import { redirectURI } from "../conf";
export interface AuthorizationResponse {
access_token: string;
token_type: string;
scope: string;
expires_in: number;
refresh_token: string;
}
async function authorizationCodePKCEGrant({ client_id, code, code_verifier }: { code: string; code_verifier: string; client_id: string }): Promise<AxiosResponse<AuthorizationResponse>> {
const body = {
client_id,
code,
code_verifier,
redirect_uri: redirectURI,
grant_type: "authorization_code",
};
try {
const res = await axios.post<AuthorizationResponse>("https://accounts.spotify.com/api/token", qs.stringify(body), {
headers: {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded",
},
});
return res;
} catch (error) {
throw error;
}
}
export default authorizationCodePKCEGrant;

View File

@ -1,13 +0,0 @@
import crypto from "crypto";
import base64url from "base64url";
export function generateCodeChallenge() {
try {
const code_verifier = crypto.randomBytes(64).toString("hex");
const base64Digest = crypto.createHash("sha256").update(code_verifier).digest("base64");
const code_challenge = base64url.fromBase64(base64Digest);
return {code_challenge, code_verifier};
} catch (error) {
throw error;
}
}

View File

@ -0,0 +1,5 @@
export function msToMinAndSec(ms: number) {
const minutes = Math.floor(ms / 60000);
const seconds:number = parseInt(((ms % 60000) / 1000).toFixed(0));
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
}

View File

@ -0,0 +1,31 @@
import { useQueryClient } from "react-query";
import { QueryCacheKeys } from "../conf";
import useSpotifyMutation from "./useSpotifyMutation";
import useSpotifyQuery from "./useSpotifyQuery";
function usePlaylistReaction() {
const queryClient = useQueryClient();
const { data: favoritePlaylists } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(QueryCacheKeys.userPlaylists, (spotifyApi) =>
spotifyApi.getUserPlaylists().then((userPlaylists) => userPlaylists.body.items)
);
const { mutate: reactToPlaylist } = useSpotifyMutation<{}, SpotifyApi.PlaylistObjectSimplified>(
(spotifyApi, { id }) => spotifyApi[isFavorite(id) ? "unfollowPlaylist" : "followPlaylist"](id).then((res) => res.body),
{
onSuccess(_, playlist) {
queryClient.setQueryData<SpotifyApi.PlaylistObjectSimplified[]>(
QueryCacheKeys.userPlaylists,
isFavorite(playlist.id) ? (old) => (old ?? []).filter((oldPlaylist) => oldPlaylist.id !== playlist.id) : (old) => [...(old ?? []), playlist]
);
},
}
);
const favoritePlaylistIds = favoritePlaylists?.map((playlist) => playlist.id);
function isFavorite(playlistId: string) {
return favoritePlaylistIds?.includes(playlistId);
}
return { reactToPlaylist, isFavorite, favoritePlaylists };
}
export default usePlaylistReaction;