mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45: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
|
Exec=AppRun
|
||||||
Icon=default
|
Icon=default
|
||||||
Comment=A music streaming app combining the power of Spotify & Youtube
|
Comment=A music streaming app combining the power of Spotify & Youtube
|
||||||
Terminal=true
|
Terminal=false
|
||||||
Categories=Music;
|
Categories=Music;
|
||||||
|
7573
package-lock.json
generated
7573
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,7 @@
|
|||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"base64url": "^3.0.1",
|
"base64url": "^3.0.1",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
|
"color": "^3.1.3",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"du": "^1.0.0",
|
"du": "^1.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"@babel/preset-react": "^7.10.4",
|
"@babel/preset-react": "^7.10.4",
|
||||||
"@babel/preset-typescript": "^7.10.4",
|
"@babel/preset-typescript": "^7.10.4",
|
||||||
"@nodegui/packer": "^1.4.1",
|
"@nodegui/packer": "^1.4.1",
|
||||||
|
"@types/color": "^3.0.1",
|
||||||
"@types/du": "^1.0.0",
|
"@types/du": "^1.0.0",
|
||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.11",
|
||||||
"@types/is-url": "^1.2.28",
|
"@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");
|
console.log("Server is running");
|
||||||
spotifyApi.setClientId(credentials.clientId);
|
spotifyApi.setClientId(credentials.clientId);
|
||||||
spotifyApi.setClientSecret(credentials.clientSecret);
|
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)
|
console.error("Opening IPC connection with browser failed: ", e)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import playerContext from "../context/playerContext";
|
import playerContext from "../context/playerContext";
|
||||||
import { TrackButton } from "./PlaylistView";
|
import { TrackButton, TrackTableIndex } from "./PlaylistView";
|
||||||
|
|
||||||
function CurrentPlaylist() {
|
function CurrentPlaylist() {
|
||||||
const { currentPlaylist, currentTrack, setCurrentTrack } = useContext(playerContext);
|
const { currentPlaylist, currentTrack, setCurrentTrack } = useContext(playerContext);
|
||||||
@ -12,7 +12,8 @@ function CurrentPlaylist() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style="flex: 1; flex-direction: 'column';">
|
<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;`}>
|
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||||
<View style={`flex-direction:column; flex: 1;`}>
|
<View style={`flex-direction:column; flex: 1;`}>
|
||||||
{currentPlaylist?.tracks.map(({ track }, index) => {
|
{currentPlaylist?.tracks.map(({ track }, index) => {
|
||||||
@ -20,9 +21,10 @@ function CurrentPlaylist() {
|
|||||||
<TrackButton
|
<TrackButton
|
||||||
key={index + track.id}
|
key={index + track.id}
|
||||||
active={currentTrack?.id === track.id}
|
active={currentTrack?.id === track.id}
|
||||||
artist={track.artists.map((x) => x.name).join(", ")}
|
track={track}
|
||||||
name={track.name}
|
index={index}
|
||||||
on={{ clicked: () => setCurrentTrack(track) }}
|
on={{ MouseButtonRelease: () => setCurrentTrack(track) }}
|
||||||
|
onTrackClick={() => {}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import React from "react";
|
import React, { useContext, useMemo, useState } from "react";
|
||||||
import { Button, ScrollArea, BoxView, View } from "@nodegui/react-nodegui";
|
import { Button, ScrollArea, View, Text } from "@nodegui/react-nodegui";
|
||||||
import { useHistory } from "react-router";
|
import { useHistory } from "react-router";
|
||||||
import CachedImage from "./shared/CachedImage";
|
import CachedImage from "./shared/CachedImage";
|
||||||
import { CursorShape, Direction } from "@nodegui/nodegui";
|
import { CursorShape, QIcon, QMouseEvent } from "@nodegui/nodegui";
|
||||||
import { QueryCacheKeys } from "../conf";
|
import { QueryCacheKeys } from "../conf";
|
||||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||||
import ErrorApplet from "./shared/ErrorApplet";
|
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() {
|
function Home() {
|
||||||
const { data: categories, isError, isRefetchError, refetch } = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
|
const { data: categories, isError, isRefetchError, refetch } = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
|
||||||
@ -33,32 +40,6 @@ interface CategoryCardProps {
|
|||||||
name: string;
|
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 = `
|
const categoryStylesheet = `
|
||||||
#container{
|
#container{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -86,25 +67,81 @@ const categoryStylesheet = `
|
|||||||
text-decoration: underline;
|
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 {
|
interface PlaylistCardProps {
|
||||||
thumbnail: string;
|
playlist: SpotifyApi.PlaylistObjectSimplified;
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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() {
|
const handlePlaylistPlayPause = async () => {
|
||||||
history.push(`/playlist/${id}`, { name, thumbnail });
|
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 = `
|
const playlistStyleSheet = `
|
||||||
#playlist-container{
|
#playlist-container{
|
||||||
max-width: 250px;
|
width: 150px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 2px;
|
padding: 5px;
|
||||||
|
min-height: 150px;
|
||||||
|
background-color: ${bgColor1};
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
#playlist-container:hover{
|
#playlist-container:hover{
|
||||||
border: 1px solid green;
|
border: 1px solid green;
|
||||||
@ -115,8 +152,50 @@ export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardPro
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}>
|
<View
|
||||||
<CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />
|
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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ import { QueryCacheKeys } from "../conf";
|
|||||||
import playerContext from "../context/playerContext";
|
import playerContext from "../context/playerContext";
|
||||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||||
import { PlaylistCard } from "./Home";
|
import { PlaylistCard } from "./Home";
|
||||||
import { PlaylistSimpleControls, TrackButton } from "./PlaylistView";
|
import { PlaylistSimpleControls, TrackButton, TrackTableIndex } from "./PlaylistView";
|
||||||
import { TabMenuItem } from "./TabMenu";
|
import { TabMenuItem } from "./TabMenu";
|
||||||
|
|
||||||
function Library() {
|
function Library() {
|
||||||
@ -39,7 +39,7 @@ function UserPlaylists() {
|
|||||||
<ScrollArea style="flex: 1; border: none;">
|
<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';">
|
<View style="flex: 1; flex-direction: 'row'; flex-wrap: 'wrap'; justify-content: 'space-evenly'; width: 330px; align-items: 'center';">
|
||||||
{userPlaylists?.map((playlist, index) => (
|
{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>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@ -53,10 +53,10 @@ function UserSavedTracks() {
|
|||||||
);
|
);
|
||||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
|
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
|
||||||
|
|
||||||
function handlePlaylistPlayPause() {
|
function handlePlaylistPlayPause(index?: number) {
|
||||||
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
|
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
|
||||||
setCurrentPlaylist({ id: userSavedPlaylistId, name: "Liked Tracks", thumbnail: "https://nerdist.com/wp-content/uploads/2020/07/maxresdefault.jpg", tracks: 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 {
|
} else {
|
||||||
setCurrentPlaylist(undefined);
|
setCurrentPlaylist(undefined);
|
||||||
setCurrentTrack(undefined);
|
setCurrentTrack(undefined);
|
||||||
@ -66,19 +66,21 @@ function UserSavedTracks() {
|
|||||||
return (
|
return (
|
||||||
<View style="flex: 1; flex-direction: 'column';">
|
<View style="flex: 1; flex-direction: 'column';">
|
||||||
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === userSavedPlaylistId} />
|
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === userSavedPlaylistId} />
|
||||||
|
<TrackTableIndex/>
|
||||||
<ScrollArea style="flex: 1; border: none;">
|
<ScrollArea style="flex: 1; border: none;">
|
||||||
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
|
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
|
||||||
{userTracks?.map(({ track }, index) => (
|
{userTracks?.map(({ track }, index) => (
|
||||||
<TrackButton
|
<TrackButton
|
||||||
key={index+track.id}
|
key={index+track.id}
|
||||||
active={currentPlaylist?.id === userSavedPlaylistId && currentTrack?.id === track.id}
|
active={currentPlaylist?.id === userSavedPlaylistId && currentTrack?.id === track.id}
|
||||||
artist={track.artists.map((x) => x.name).join(", ")}
|
track={track}
|
||||||
name={track.name}
|
index={index}
|
||||||
on={{
|
on={{
|
||||||
clicked() {
|
MouseButtonRelease() {
|
||||||
setCurrentTrack(track);
|
setCurrentTrack(track);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onTrackClick={()=>handlePlaylistPlayPause(index)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
@ -26,9 +26,8 @@ export const audioPlayer = new NodeMpv(
|
|||||||
function Player(): ReactElement {
|
function Player(): ReactElement {
|
||||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||||
const initVolume = parseFloat(localStorage.getItem("volume") ?? "55");
|
|
||||||
const [isPaused, setIsPaused] = useState(true);
|
const [isPaused, setIsPaused] = useState(true);
|
||||||
const [volume, setVolume] = useState<number>(initVolume);
|
const [volume, setVolume] = useState<number>(55);
|
||||||
const [totalDuration, setTotalDuration] = useState(0);
|
const [totalDuration, setTotalDuration] = useState(0);
|
||||||
const [shuffle, setShuffle] = useState<boolean>(false);
|
const [shuffle, setShuffle] = useState<boolean>(false);
|
||||||
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
||||||
@ -54,10 +53,9 @@ function Player(): ReactElement {
|
|||||||
try {
|
try {
|
||||||
if (!playerRunning) {
|
if (!playerRunning) {
|
||||||
await audioPlayer.start();
|
await audioPlayer.start();
|
||||||
await audioPlayer.volume(initVolume);
|
await audioPlayer.volume(volume);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start audio player", error);
|
|
||||||
showError(error, "[Failed starting audio player]: ");
|
showError(error, "[Failed starting audio player]: ");
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@ -90,6 +88,11 @@ function Player(): ReactElement {
|
|||||||
})();
|
})();
|
||||||
}, [currentTrack]);
|
}, [currentTrack]);
|
||||||
|
|
||||||
|
// changing shuffle to default
|
||||||
|
useEffect(() => {
|
||||||
|
setShuffle(false);
|
||||||
|
}, [currentPlaylist])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (playerRunning) {
|
if (playerRunning) {
|
||||||
audioPlayer.volume(volume);
|
audioPlayer.volume(volume);
|
||||||
@ -186,7 +189,7 @@ function Player(): ReactElement {
|
|||||||
<Text ref={titleRef} wordWrap>
|
<Text ref={titleRef} wordWrap>
|
||||||
{artistsNames && currentTrack
|
{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>`}
|
: `<b>Oh, dear don't waste time</b>`}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -49,9 +49,7 @@ function PlaylistGenreView() {
|
|||||||
return (
|
return (
|
||||||
<PlaylistCard
|
<PlaylistCard
|
||||||
key={index + playlist.id}
|
key={index + playlist.id}
|
||||||
id={playlist.id}
|
playlist={playlist}
|
||||||
name={playlist.name}
|
|
||||||
thumbnail={playlist.images[0].url}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -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 { View, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
|
||||||
import BackButton from "./BackButton";
|
import BackButton from "./BackButton";
|
||||||
import { useLocation, useParams } from "react-router";
|
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 { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
|
||||||
import playerContext from "../context/playerContext";
|
import playerContext from "../context/playerContext";
|
||||||
import IconButton from "./shared/IconButton";
|
import IconButton from "./shared/IconButton";
|
||||||
import { heartRegular, play, stop } from "../icons";
|
import { heart, heartRegular, pause, play, stop } from "../icons";
|
||||||
import { audioPlayer } from "./Player";
|
import { audioPlayer } from "./Player";
|
||||||
import { QueryCacheKeys } from "../conf";
|
import { QueryCacheKeys } from "../conf";
|
||||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||||
|
import useTrackReaction from "../hooks/useTrackReaction";
|
||||||
|
import usePlaylistReaction from "../hooks/usePlaylistReaction";
|
||||||
|
import { msToMinAndSec } from "../helpers/msToMin:sec";
|
||||||
|
|
||||||
export interface PlaylistTrackRes {
|
export interface PlaylistTrackRes {
|
||||||
name: string;
|
name: string;
|
||||||
@ -21,17 +24,20 @@ const PlaylistView: FC = () => {
|
|||||||
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
|
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const location = useLocation<{ name: string; thumbnail: 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[]>(
|
const { data: tracks, isSuccess, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
||||||
[QueryCacheKeys.playlistTracks, params.id],
|
[QueryCacheKeys.playlistTracks, params.id],
|
||||||
(spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
|
(spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
|
||||||
{ initialData: [] }
|
{ initialData: [] }
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePlaylistPlayPause = () => {
|
const handlePlaylistPlayPause = (index?: number) => {
|
||||||
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
|
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
|
||||||
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
||||||
setCurrentTrack(tracks[0].track);
|
setCurrentTrack(tracks[index ?? 0].track);
|
||||||
} else {
|
} else {
|
||||||
audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error));
|
audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error));
|
||||||
setCurrentTrack(undefined);
|
setCurrentTrack(undefined);
|
||||||
@ -45,8 +51,14 @@ const PlaylistView: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={`flex: 1; flex-direction: 'column';`}>
|
<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>
|
<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;`}>
|
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||||
<View style={`flex-direction:column; flex: 1;`}>
|
<View style={`flex-direction:column; flex: 1;`}>
|
||||||
{isLoading && <Text>{`Loading Tracks...`}</Text>}
|
{isLoading && <Text>{`Loading Tracks...`}</Text>}
|
||||||
@ -67,10 +79,15 @@ const PlaylistView: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<TrackButton
|
<TrackButton
|
||||||
key={index + track.id}
|
key={index + track.id}
|
||||||
|
track={track}
|
||||||
|
index={index}
|
||||||
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
|
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
|
||||||
artist={track.artists.map((x) => x.name).join(", ")}
|
on={{
|
||||||
name={track.name}
|
MouseButtonRelease: () => trackClickHandler(track),
|
||||||
on={{ clicked: () => trackClickHandler(track) }}
|
}}
|
||||||
|
onTrackClick={() => {
|
||||||
|
handlePlaylistPlayPause(index);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -81,36 +98,95 @@ const PlaylistView: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface TrackButtonProps {
|
export interface TrackButtonProps {
|
||||||
name: string;
|
track: SpotifyApi.TrackObjectFull;
|
||||||
artist: string;
|
on: Partial<QWidgetSignals | WidgetEventListeners>;
|
||||||
|
onTrackClick?: QAbstractButtonSignals["clicked"];
|
||||||
active: boolean;
|
active: boolean;
|
||||||
on: Partial<QAbstractButtonSignals | WidgetEventListeners>;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TrackButton: FC<TrackButtonProps> = ({ name, artist, on, active }) => {
|
export const TrackButton: FC<TrackButtonProps> = ({ track, active, index, on, onTrackClick }) => {
|
||||||
return <Button id={`${active ? "active" : ""}`} on={on} text={`${name} -- ${artist}`} styleSheet={trackButtonStyle} />;
|
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 = `
|
const trackButtonStyle = `
|
||||||
#active{
|
#active{
|
||||||
background-color: orange;
|
background-color: #34eb71;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
#track-button:hover, #active:hover{
|
||||||
|
background-color: rgba(229, 224, 224, 0.48);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default PlaylistView;
|
export default PlaylistView;
|
||||||
|
|
||||||
interface PlaylistSimpleControlsProps {
|
export function TrackTableIndex() {
|
||||||
handlePlaylistPlayPause: () => void;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PlaylistSimpleControls({ handlePlaylistPlayPause, isActive }: PlaylistSimpleControlsProps) {
|
|
||||||
return (
|
return (
|
||||||
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
|
<View>
|
||||||
<BackButton />
|
<Text style="padding: 5px;">{`<h3>#</h3>`}</Text>
|
||||||
<IconButton icon={new QIcon(heartRegular)} />
|
<Text style="width: '35%';">{`<h3>Title</h3>`}</Text>
|
||||||
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(isActive ? stop : play)} />
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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