mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Error Applet component, little incomplete documentation & incomplete tabmenu added
This commit is contained in:
parent
faea24e86e
commit
fe39ab0ffd
75
README.md
75
README.md
@ -1,63 +1,44 @@
|
|||||||
# react-nodegui-starter
|
# Spotube
|
||||||
|
Spotube is a [qt](https://qt.io) based lightweight spotify client which uses [nodegui/react-nodegui](https://github.com/nodegui/react-nodegui) as frontend & nodejs as backend. It utilizes the power of Spotify & Youtube's public API & creates a hazardless, performant & resource friendly User Experience
|
||||||
|
|
||||||
**Clone and run for a quick way to see React NodeGui in action.**
|
## Features
|
||||||
|
Following are the features that currently spotube offers:
|
||||||
|
- Open Source
|
||||||
|
- No telementry, diagnostics or user data collection
|
||||||
|
- Lightweight & resource friendly
|
||||||
|
- Near native performance & seemless with default desktop themes (Win10, Win7, OSX, QT-default)
|
||||||
|
- Playback control is on user's machine instead of server based
|
||||||
|
- Small size & less data hungry
|
||||||
|
- No spotify or youtube ads since it uses all public & free APIs (But it's recommended to support the creators by watching/liking/subscribing to the artists youtube channel or add as favourite track in spotify. Mostly buying spotify premium is the best way to support their valuable creations)
|
||||||
|
|
||||||
<img alt="logo" src="https://github.com/nodegui/react-nodegui-starter/raw/master/assets/demo.png" height="500" />
|
## Installation
|
||||||
|
I'm always releasing newer versions of binary of the software each 2-3 month with minor changes & each 6-8 month with major changes. Grab the binaries
|
||||||
|
|
||||||
## To Use
|
Windows: [.exe]()
|
||||||
|
|
||||||
To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line:
|
OSX: **I hate apple** (Just kidding, actually don't have a mac)
|
||||||
|
|
||||||
```bash
|
Linux: [.appimage]()
|
||||||
# Clone this repository
|
|
||||||
git clone https://github.com/nodegui/react-nodegui-starter
|
|
||||||
# Install CMake
|
|
||||||
brew install cmake
|
|
||||||
# Go into the repository
|
|
||||||
cd react-nodegui-starter
|
|
||||||
# Install dependencies
|
|
||||||
npm install
|
|
||||||
# Run the dev server
|
|
||||||
npm run dev
|
|
||||||
# Open andother terminal and run the app
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation & Resources for learning React NodeGui
|
|
||||||
|
|
||||||
- [Documentation](https://react.nodegui.org) - all of React NodeGui's documentation.
|
**I'll/try to upload the package binaries to linux debian/arch/ubuntu/snap/flatpack/redhat stores or software centers or repositories**
|
||||||
- [NodeGui](https://nodegui.org) - all of NodeGui's documentation.
|
|
||||||
|
|
||||||
## Packaging app as a distributable
|
## Configuration
|
||||||
|
There are some configurations that needs to be done to start using this software
|
||||||
|
|
||||||
In order to distribute your finished app, you can use [@nodegui/packer](https://github.com/nodegui/packer)
|
You need a spotify account & a web app for
|
||||||
|
|
||||||
### Step 1: (_**Run this command only once**_)
|
- clientId
|
||||||
|
- clientSecret
|
||||||
|
|
||||||
```sh
|
See these screenshots:
|
||||||
npx nodegui-packer --init MyAppName
|
|
||||||
```
|
|
||||||
|
|
||||||
This will produce the deploy directory containing the template. You can modify this to suite your needs. Like add icons, change the name, description and add other native features or dependencies. Make sure you commit this directory.
|
[1]()
|
||||||
|
|
||||||
### Step 2: (_**Run this command every time you want to build a new distributable**_)
|
[2]()
|
||||||
|
|
||||||
Next you can run the pack command:
|
[3]()
|
||||||
|
|
||||||
```sh
|
[4]()
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
This will produce the js bundle along with assets inside the `./dist` directory
|
**[Important]!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
|
||||||
|
|
||||||
```sh
|
|
||||||
npx nodegui-packer --pack ./dist
|
|
||||||
```
|
|
||||||
|
|
||||||
This will build the distributable using @nodegui/packer based on your template. The output of the command is found under the build directory. You should gitignore the build directory.
|
|
||||||
|
|
||||||
More details about packer can be found here: https://github.com/nodegui/packer
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
327
spotube.sublime-workspace
Normal file
327
spotube.sublime-workspace
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
{
|
||||||
|
"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":
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -139,12 +139,10 @@ function RootApp() {
|
|||||||
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}>
|
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}>
|
||||||
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
|
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
{/* <View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}> */}
|
<View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}>
|
||||||
<BoxView direction={Direction.TopToBottom}>
|
|
||||||
<Routes />
|
<Routes />
|
||||||
{isLoggedIn && <Player />}
|
{isLoggedIn && <Player />}
|
||||||
</BoxView>
|
</View>
|
||||||
{/* </View> */}
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</playerContext.Provider>
|
</playerContext.Provider>
|
||||||
</authContext.Provider>
|
</authContext.Provider>
|
||||||
|
@ -1,38 +1,43 @@
|
|||||||
import React, { useContext, useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { Button, ScrollArea, BoxView } from "@nodegui/react-nodegui";
|
import { Button, ScrollArea, BoxView, View } 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, Direction } from "@nodegui/nodegui";
|
||||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
import { QueryCacheKeys } from "../conf";
|
||||||
import showError from "../helpers/showError";
|
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||||
import authContext from "../context/authContext";
|
import ErrorApplet from "./shared/ErrorApplet";
|
||||||
import useSpotifyApiError from "../hooks/useSpotifyApiError";
|
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const spotifyApi = useSpotifyApi();
|
const {
|
||||||
const { access_token } = useContext(authContext);
|
data: categories,
|
||||||
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
|
isError,
|
||||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
isRefetchError,
|
||||||
|
refetch,
|
||||||
useEffect(() => {
|
} = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
|
||||||
if (categories.length === 0) {
|
QueryCacheKeys.categories,
|
||||||
|
(spotifyApi) =>
|
||||||
spotifyApi
|
spotifyApi
|
||||||
.getCategories({ country: "US" })
|
.getCategories({ country: "US" })
|
||||||
.then((categoriesReceived) => setCategories(categoriesReceived.body.categories.items))
|
.then((categoriesReceived) => categoriesReceived.body.categories.items),
|
||||||
.catch((error) => {
|
{ initialData: [] }
|
||||||
showError(error, "[Spotify genre loading failed]: ");
|
);
|
||||||
handleSpotifyError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [access_token]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||||
<BoxView direction={Direction.TopToBottom}>
|
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
|
||||||
{categories.map((category, index) => {
|
{(isError || isRefetchError) && (
|
||||||
return <CategoryCard key={index+category.id} id={category.id} name={category.name} />;
|
<ErrorApplet message="Failed to query genres" reload={refetch} helps />
|
||||||
|
)}
|
||||||
|
{categories?.map((category, index) => {
|
||||||
|
return (
|
||||||
|
<CategoryCard
|
||||||
|
key={index + category.id}
|
||||||
|
id={category.id}
|
||||||
|
name={category.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</BoxView>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -46,40 +51,52 @@ interface CategoryCardProps {
|
|||||||
|
|
||||||
const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
|
const { data: playlists, isError } = useSpotifyQuery<
|
||||||
const spotifyApi = useSpotifyApi();
|
SpotifyApi.PlaylistObjectSimplified[]
|
||||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
>(
|
||||||
|
[QueryCacheKeys.categoryPlaylists, id],
|
||||||
useEffect(() => {
|
(spotifyApi) =>
|
||||||
if (playlists.length === 0) {
|
|
||||||
spotifyApi
|
spotifyApi
|
||||||
.getPlaylistsForCategory(id, { limit: 4 })
|
.getPlaylistsForCategory(id, { limit: 4 })
|
||||||
.then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
|
.then((playlistsRes) => playlistsRes.body.playlists.items),
|
||||||
.catch((error) => {
|
{ initialData: [] }
|
||||||
showError(error, `[Failed to get playlists of category ${name}]: `);
|
);
|
||||||
handleSpotifyError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function goToGenre() {
|
function goToGenre() {
|
||||||
history.push(`/genre/playlists/${id}`, { name });
|
history.push(`/genre/playlists/${id}`, { name });
|
||||||
}
|
}
|
||||||
|
if (isError) {
|
||||||
|
return <></ >;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<BoxView id="container" styleSheet={categoryStylesheet} direction={Direction.TopToBottom}>
|
<View id="container" styleSheet={categoryStylesheet}>
|
||||||
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
|
<Button
|
||||||
<BoxView direction={Direction.LeftToRight}>
|
id="anchor-heading"
|
||||||
{playlists.map((playlist, index) => {
|
cursor={CursorShape.PointingHandCursor}
|
||||||
return <PlaylistCard key={index+playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</BoxView>
|
</View>
|
||||||
</BoxView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const categoryStylesheet = `
|
const categoryStylesheet = `
|
||||||
#container{
|
#container{
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: 'center';
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
#anchor-heading{
|
#anchor-heading{
|
||||||
@ -89,7 +106,12 @@ const categoryStylesheet = `
|
|||||||
outline: none;
|
outline: none;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
#child-view{
|
||||||
|
flex: 1;
|
||||||
|
justify-content: 'space-around';
|
||||||
|
align-items: 'center';
|
||||||
}
|
}
|
||||||
#anchor-heading:hover{
|
#anchor-heading:hover{
|
||||||
border: none;
|
border: none;
|
||||||
@ -104,7 +126,8 @@ interface PlaylistCardProps {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardProps) => {
|
export const PlaylistCard = React.memo(
|
||||||
|
({ id, name, thumbnail }: PlaylistCardProps) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
function gotoPlaylist() {
|
function gotoPlaylist() {
|
||||||
@ -113,9 +136,9 @@ export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardPro
|
|||||||
|
|
||||||
const playlistStyleSheet = `
|
const playlistStyleSheet = `
|
||||||
#playlist-container{
|
#playlist-container{
|
||||||
max-width: 150px;
|
max-width: 250px;
|
||||||
max-height: 150px;
|
flex-direction: column;
|
||||||
min-height: 150px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
#playlist-container:hover{
|
#playlist-container:hover{
|
||||||
border: 1px solid green;
|
border: 1px solid green;
|
||||||
@ -126,8 +149,19 @@ export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardPro
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxView size={{height: 150, width: 150, fixed: true}} direction={Direction.TopToBottom} 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"
|
||||||
</BoxView>
|
cursor={CursorShape.PointingHandCursor}
|
||||||
|
styleSheet={playlistStyleSheet}
|
||||||
|
on={{ MouseButtonRelease: gotoPlaylist }}
|
||||||
|
>
|
||||||
|
<CachedImage
|
||||||
|
src={thumbnail}
|
||||||
|
maxSize={{ height: 150, width: 150 }}
|
||||||
|
scaledContents
|
||||||
|
alt={name}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
@ -1,32 +1,21 @@
|
|||||||
import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
import { Direction } from "@nodegui/nodegui";
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
import { BoxView, ScrollArea, Text, View, GridView, GridColumn, GridRow } from "@nodegui/react-nodegui";
|
||||||
|
import React from "react";
|
||||||
import { useLocation, useParams } from "react-router";
|
import { useLocation, useParams } from "react-router";
|
||||||
import authContext from "../context/authContext";
|
import { QueryCacheKeys } from "../conf";
|
||||||
import showError from "../helpers/showError";
|
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
|
||||||
import useSpotifyApiError from "../hooks/useSpotifyApiError";
|
|
||||||
import BackButton from "./BackButton";
|
import BackButton from "./BackButton";
|
||||||
import { PlaylistCard } from "./Home";
|
import { PlaylistCard } from "./Home";
|
||||||
|
|
||||||
function PlaylistGenreView() {
|
function PlaylistGenreView() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const location = useLocation<{ name: string }>();
|
const location = useLocation<{ name: string }>();
|
||||||
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
|
const { data: playlists } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
||||||
const { access_token, isLoggedIn } = useContext(authContext);
|
[QueryCacheKeys.genrePlaylists, id],
|
||||||
const spotifyApi = useSpotifyApi();
|
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id)
|
||||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
.then((playlistsRes) => playlistsRes.body.playlists.items),
|
||||||
|
{ initialData: [] }
|
||||||
useEffect(() => {
|
)
|
||||||
if (playlists.length === 0 && access_token) {
|
|
||||||
spotifyApi
|
|
||||||
.getPlaylistsForCategory(id)
|
|
||||||
.then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
|
|
||||||
.catch((error) => {
|
|
||||||
showError(error, `[Failed to get playlists of category ${location.state.name} for]: `);
|
|
||||||
handleSpotifyError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [access_token]);
|
|
||||||
|
|
||||||
const playlistGenreViewStylesheet = `
|
const playlistGenreViewStylesheet = `
|
||||||
#genre-container{
|
#genre-container{
|
||||||
@ -56,9 +45,15 @@ function PlaylistGenreView() {
|
|||||||
<Text id="heading">{`<h2>${location.state.name}</h2>`}</Text>
|
<Text id="heading">{`<h2>${location.state.name}</h2>`}</Text>
|
||||||
<ScrollArea id="scroll-view">
|
<ScrollArea id="scroll-view">
|
||||||
<View id="child-container">
|
<View id="child-container">
|
||||||
{isLoggedIn &&
|
{playlists?.map((playlist, index) => {
|
||||||
playlists.map((playlist, index) => {
|
return (
|
||||||
return <PlaylistCard key={((index * Date.now()) / Math.random()) * 100} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
|
<PlaylistCard
|
||||||
|
key={index + playlist.id}
|
||||||
|
id={playlist.id}
|
||||||
|
name={playlist.name}
|
||||||
|
thumbnail={playlist.images[0].url}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { FC, useContext } from "react";
|
import React, { FC, useContext } from "react";
|
||||||
import { BoxView, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
|
import { BoxView, 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 { Direction, QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
|
import { Direction, QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
|
||||||
@ -44,15 +44,15 @@ const PlaylistView: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxView direction={Direction.TopToBottom}>
|
<View style={`flex: 1; flex-direction: 'column'; flex-grow: 1;`}>
|
||||||
<BoxView style={`max-width: 150px;`}>
|
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<IconButton icon={new QIcon(heartRegular)} />
|
<IconButton icon={new QIcon(heartRegular)} />
|
||||||
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(currentPlaylist?.id === params.id ? stop : play)} />
|
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(currentPlaylist?.id === params.id ? stop : play)} />
|
||||||
</BoxView>
|
</View>
|
||||||
<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>
|
||||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
<ScrollArea style={`flx:1; flex-grow: 1; border: none;`}>
|
||||||
<BoxView /* style={`flex-direction:column;`} */ direction={Direction.TopToBottom}>
|
<View style={`flex-direction:column; flex: 1;`}>
|
||||||
{isLoading && <Text>{`Loading Tracks...`}</Text>}
|
{isLoading && <Text>{`Loading Tracks...`}</Text>}
|
||||||
{isError && (
|
{isError && (
|
||||||
<>
|
<>
|
||||||
@ -78,9 +78,9 @@ const PlaylistView: FC = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</BoxView>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</BoxView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
47
src/components/TabMenu.tsx
Normal file
47
src/components/TabMenu.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {Route} from "react-router"
|
||||||
|
import {View, Button} from "@nodegui/react-nodegui"
|
||||||
|
import {QIcon} from "@nodegui/nodegui"
|
||||||
|
|
||||||
|
function TabMenu(){
|
||||||
|
return (
|
||||||
|
<View id="tabmenu" styleSheet={tabBarStylesheet}>
|
||||||
|
<TabMenuItem title="Browse"/>
|
||||||
|
<TabMenuItem title="Library"/>
|
||||||
|
<TabMenuItem title="Currently Playing"/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabBarStylesheet = `
|
||||||
|
#tabmenu{
|
||||||
|
flex-direction: 'column';
|
||||||
|
align-items: 'center';
|
||||||
|
max-width: 225px;
|
||||||
|
}
|
||||||
|
#tabmenu-item{
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
#tabmenu-item:hover{
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
#tabmenu-item:active{
|
||||||
|
color: #59ff88;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default TabMenu;
|
||||||
|
|
||||||
|
interface TabMenuItemProps{
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* path to the icon in string
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabMenuItem({icon, title}:TabMenuItemProps){
|
||||||
|
return (
|
||||||
|
<Button id="tabmenu-item" text={title}/>
|
||||||
|
)
|
||||||
|
}
|
24
src/components/shared/ErrorApplet.tsx
Normal file
24
src/components/shared/ErrorApplet.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { View, Button, Text } from "@nodegui/react-nodegui";
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
interface ErrorAppletProps {
|
||||||
|
message?: string;
|
||||||
|
reload: Function;
|
||||||
|
helps?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorApplet({ message, reload, helps }: ErrorAppletProps) {
|
||||||
|
return (
|
||||||
|
<View style="flex-direction: 'column'; align-items: 'center';">
|
||||||
|
<Text openExternalLinks>{`
|
||||||
|
<h3>${message ? message : 'An error occured'}</h3>
|
||||||
|
${helps ? `<p>Check if you're connected to internet & then try again. If the issue still persists ask for help or create a <a href="https://github.com/krtirtho/spotube/issues">issue</a>
|
||||||
|
</p>`: ``
|
||||||
|
}
|
||||||
|
`}</Text>
|
||||||
|
<Button on={{ clicked() { reload() } }} text="Reload" />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorApplet;
|
@ -9,5 +9,6 @@ export const redirectURI = "http://localhost:4304/auth/spotify/callback"
|
|||||||
export enum QueryCacheKeys{
|
export enum QueryCacheKeys{
|
||||||
categories="categories",
|
categories="categories",
|
||||||
categoryPlaylists = "categoryPlaylists",
|
categoryPlaylists = "categoryPlaylists",
|
||||||
playlistTracks="playlistTracks"
|
genrePlaylists="genrePlaylists",
|
||||||
|
playlistTracks="playlistTracks",
|
||||||
}
|
}
|
@ -1,29 +0,0 @@
|
|||||||
import { DependencyList, useContext, useEffect } from "react";
|
|
||||||
import authContext from "../context/authContext";
|
|
||||||
import { CredentialKeys } from "../app";
|
|
||||||
import SpotifyWebApi from "spotify-web-api-node";
|
|
||||||
|
|
||||||
interface UseAccessTokenResult {
|
|
||||||
access_token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (spotifyApi: SpotifyWebApi, deps: DependencyList = []): UseAccessTokenResult => {
|
|
||||||
const { access_token, expires_in, isLoggedIn, setExpires_in, setAccess_token } = useContext(authContext);
|
|
||||||
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isExpiredToken = Date.now() > expires_in;
|
|
||||||
if (isLoggedIn && isExpiredToken && refreshToken) {
|
|
||||||
spotifyApi.setRefreshToken(refreshToken);
|
|
||||||
spotifyApi
|
|
||||||
.refreshAccessToken()
|
|
||||||
.then(({ body: { access_token, expires_in } }) => {
|
|
||||||
setAccess_token(access_token);
|
|
||||||
setExpires_in(expires_in);
|
|
||||||
})
|
|
||||||
.catch();
|
|
||||||
}
|
|
||||||
}, deps);
|
|
||||||
|
|
||||||
return { access_token };
|
|
||||||
};
|
|
@ -2,19 +2,38 @@ import chalk from "chalk";
|
|||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { CredentialKeys } from "../app";
|
import { CredentialKeys } from "../app";
|
||||||
import authContext from "../context/authContext";
|
import authContext from "../context/authContext";
|
||||||
|
import showError from "../helpers/showError";
|
||||||
import spotifyApi from "../initializations/spotifyApi";
|
import spotifyApi from "../initializations/spotifyApi";
|
||||||
|
|
||||||
function useSpotifyApi() {
|
function useSpotifyApi() {
|
||||||
const { access_token, clientId, clientSecret, isLoggedIn } = useContext(authContext);
|
const {
|
||||||
|
access_token,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
isLoggedIn,
|
||||||
|
setAccess_token,
|
||||||
|
} = useContext(authContext);
|
||||||
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
|
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn && clientId && clientSecret && refreshToken) {
|
if (isLoggedIn && clientId && clientSecret && refreshToken) {
|
||||||
console.log(chalk.bgCyan.black("Setting up spotify credentials"))
|
console.log(chalk.bgCyan.black("Setting up spotify credentials"));
|
||||||
spotifyApi.setClientId(clientId);
|
spotifyApi.setClientId(clientId);
|
||||||
spotifyApi.setClientSecret(clientSecret);
|
spotifyApi.setClientSecret(clientSecret);
|
||||||
spotifyApi.setRefreshToken(refreshToken);
|
spotifyApi.setRefreshToken(refreshToken);
|
||||||
|
if (!access_token) {
|
||||||
|
spotifyApi
|
||||||
|
.refreshAccessToken()
|
||||||
|
.then((token) => {
|
||||||
|
console.log(chalk.bgRedBright.yellow("Refreshing access token from useSpotifyApi"));
|
||||||
|
setAccess_token(token.body.access_token);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
showError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
spotifyApi.setAccessToken(access_token);
|
spotifyApi.setAccessToken(access_token);
|
||||||
|
console.log(chalk.bgCyan.green("Finished setting up credentials"));
|
||||||
}
|
}
|
||||||
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { QueryFunction, QueryKey, useQuery, UseQueryOptions, UseQueryResult } from "react-query";
|
import { QueryKey, useQuery, UseQueryOptions, UseQueryResult } from "react-query";
|
||||||
import SpotifyWebApi from "spotify-web-api-node";
|
import SpotifyWebApi from "spotify-web-api-node";
|
||||||
import useSpotifyApi from "./useSpotifyApi";
|
import useSpotifyApi from "./useSpotifyApi";
|
||||||
import useSpotifyApiError from "./useSpotifyApiError";
|
import useSpotifyApiError from "./useSpotifyApiError";
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
|
import {View} from "@nodegui/react-nodegui";
|
||||||
import { Route } from "react-router";
|
import { Route } from "react-router";
|
||||||
import authContext from "./context/authContext";
|
import authContext from "./context/authContext";
|
||||||
import Home from "./components/Home";
|
import Home from "./components/Home";
|
||||||
import Login from "./components/Login";
|
import Login from "./components/Login";
|
||||||
import PlaylistView from "./components/PlaylistView";
|
import PlaylistView from "./components/PlaylistView";
|
||||||
import PlaylistGenreView from "./components/PlaylistGenreView";
|
import PlaylistGenreView from "./components/PlaylistGenreView";
|
||||||
|
import TabMenu from "./components/TabMenu";
|
||||||
|
|
||||||
function Routes() {
|
function Routes() {
|
||||||
const {
|
const {
|
||||||
@ -12,11 +14,18 @@ function Routes() {
|
|||||||
} = useContext(authContext);
|
} = useContext(authContext);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Route exact path="/">
|
<Route path="/">
|
||||||
{isLoggedIn ? <Home /> : <Login />}
|
{ isLoggedIn ?
|
||||||
</Route>
|
<View style="background-color: black; flex: 1; flex-direction: 'column';">
|
||||||
|
<TabMenu />
|
||||||
|
<Route exact path="/"><Home/></Route>
|
||||||
<Route exact path="/playlist/:id"><PlaylistView /></Route>
|
<Route exact path="/playlist/:id"><PlaylistView /></Route>
|
||||||
<Route exact path="/genre/playlists/:id"><PlaylistGenreView /></Route>
|
<Route exact path="/genre/playlists/:id"><PlaylistGenreView /></Route>
|
||||||
|
</View>
|
||||||
|
: <Login/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</Route>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user