mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
follow & unfollow playlist feature, colorful playlistButton for lessening resource usage, was added
This commit is contained in:
parent
9751b4f837
commit
9df74fbe36
@ -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
7571
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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":
|
||||
{
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
});
|
||||
|
@ -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);
|
||||
@ -13,6 +13,7 @@ function CurrentPlaylist() {
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<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={() => {}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
10
src/helpers/RandomColor.ts
Normal file
10
src/helpers/RandomColor.ts
Normal 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();
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
5
src/helpers/msToMin:sec.ts
Normal file
5
src/helpers/msToMin:sec.ts
Normal 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;
|
||||
}
|
31
src/hooks/usePlaylistReaction.ts
Normal file
31
src/hooks/usePlaylistReaction.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user