mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05: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
|
||||
# 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
|
||||
```
|
||||
Linux: [.appimage]()
|
||||
|
||||
## Installation & Resources for learning React NodeGui
|
||||
|
||||
- [Documentation](https://react.nodegui.org) - all of React NodeGui's documentation.
|
||||
- [NodeGui](https://nodegui.org) - all of 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**
|
||||
|
||||
## 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
|
||||
npx nodegui-packer --init MyAppName
|
||||
```
|
||||
See these screenshots:
|
||||
|
||||
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
|
||||
npm run build
|
||||
```
|
||||
[4]()
|
||||
|
||||
This will produce the js bundle along with assets inside the `./dist` directory
|
||||
|
||||
```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
|
||||
**[Important]!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
|
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 }}>
|
||||
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/* <View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}> */}
|
||||
<BoxView direction={Direction.TopToBottom}>
|
||||
<View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}>
|
||||
<Routes />
|
||||
{isLoggedIn && <Player />}
|
||||
</BoxView>
|
||||
{/* </View> */}
|
||||
</View>
|
||||
</QueryClientProvider>
|
||||
</playerContext.Provider>
|
||||
</authContext.Provider>
|
||||
|
@ -1,38 +1,43 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Button, ScrollArea, BoxView } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
import { Button, ScrollArea, BoxView, View } from "@nodegui/react-nodegui";
|
||||
import { useHistory } from "react-router";
|
||||
import CachedImage from "./shared/CachedImage";
|
||||
import { CursorShape, Direction } from "@nodegui/nodegui";
|
||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
||||
import showError from "../helpers/showError";
|
||||
import authContext from "../context/authContext";
|
||||
import useSpotifyApiError from "../hooks/useSpotifyApiError";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import ErrorApplet from "./shared/ErrorApplet";
|
||||
|
||||
function Home() {
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const { access_token } = useContext(authContext);
|
||||
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
|
||||
useEffect(() => {
|
||||
if (categories.length === 0) {
|
||||
const {
|
||||
data: categories,
|
||||
isError,
|
||||
isRefetchError,
|
||||
refetch,
|
||||
} = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
|
||||
QueryCacheKeys.categories,
|
||||
(spotifyApi) =>
|
||||
spotifyApi
|
||||
.getCategories({ country: "US" })
|
||||
.then((categoriesReceived) => setCategories(categoriesReceived.body.categories.items))
|
||||
.catch((error) => {
|
||||
showError(error, "[Spotify genre loading failed]: ");
|
||||
handleSpotifyError(error);
|
||||
});
|
||||
}
|
||||
}, [access_token]);
|
||||
.then((categoriesReceived) => categoriesReceived.body.categories.items),
|
||||
{ initialData: [] }
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||
<BoxView direction={Direction.TopToBottom}>
|
||||
{categories.map((category, index) => {
|
||||
return <CategoryCard key={index+category.id} id={category.id} name={category.name} />;
|
||||
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
|
||||
{(isError || isRefetchError) && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -46,40 +51,52 @@ interface CategoryCardProps {
|
||||
|
||||
const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
||||
const history = useHistory();
|
||||
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
|
||||
useEffect(() => {
|
||||
if (playlists.length === 0) {
|
||||
const { data: playlists, isError } = useSpotifyQuery<
|
||||
SpotifyApi.PlaylistObjectSimplified[]
|
||||
>(
|
||||
[QueryCacheKeys.categoryPlaylists, id],
|
||||
(spotifyApi) =>
|
||||
spotifyApi
|
||||
.getPlaylistsForCategory(id, { limit: 4 })
|
||||
.then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
|
||||
.catch((error) => {
|
||||
showError(error, `[Failed to get playlists of category ${name}]: `);
|
||||
handleSpotifyError(error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
.then((playlistsRes) => playlistsRes.body.playlists.items),
|
||||
{ initialData: [] }
|
||||
);
|
||||
|
||||
function goToGenre() {
|
||||
history.push(`/genre/playlists/${id}`, { name });
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <></ >;
|
||||
}
|
||||
return (
|
||||
<BoxView id="container" styleSheet={categoryStylesheet} direction={Direction.TopToBottom}>
|
||||
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
|
||||
<BoxView direction={Direction.LeftToRight}>
|
||||
{playlists.map((playlist, index) => {
|
||||
return <PlaylistCard key={index+playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</BoxView>
|
||||
</BoxView>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const categoryStylesheet = `
|
||||
#container{
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: 'center';
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#anchor-heading{
|
||||
@ -89,7 +106,12 @@ const categoryStylesheet = `
|
||||
outline: none;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
align-self: flex-start;
|
||||
}
|
||||
#child-view{
|
||||
flex: 1;
|
||||
justify-content: 'space-around';
|
||||
align-items: 'center';
|
||||
}
|
||||
#anchor-heading:hover{
|
||||
border: none;
|
||||
@ -104,7 +126,8 @@ interface PlaylistCardProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardProps) => {
|
||||
export const PlaylistCard = React.memo(
|
||||
({ id, name, thumbnail }: PlaylistCardProps) => {
|
||||
const history = useHistory();
|
||||
|
||||
function gotoPlaylist() {
|
||||
@ -113,9 +136,9 @@ export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardPro
|
||||
|
||||
const playlistStyleSheet = `
|
||||
#playlist-container{
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
min-height: 150px;
|
||||
max-width: 250px;
|
||||
flex-direction: column;
|
||||
padding: 2px;
|
||||
}
|
||||
#playlist-container:hover{
|
||||
border: 1px solid green;
|
||||
@ -126,8 +149,19 @@ export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardPro
|
||||
`;
|
||||
|
||||
return (
|
||||
<BoxView size={{height: 150, width: 150, fixed: true}} direction={Direction.TopToBottom} id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}>
|
||||
<CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />
|
||||
</BoxView>
|
||||
<View
|
||||
id="playlist-container"
|
||||
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 React, { useContext, useEffect, useState } from "react";
|
||||
import { Direction } from "@nodegui/nodegui";
|
||||
import { BoxView, ScrollArea, Text, View, GridView, GridColumn, GridRow } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
import { useLocation, useParams } from "react-router";
|
||||
import authContext from "../context/authContext";
|
||||
import showError from "../helpers/showError";
|
||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
||||
import useSpotifyApiError from "../hooks/useSpotifyApiError";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import BackButton from "./BackButton";
|
||||
import { PlaylistCard } from "./Home";
|
||||
|
||||
function PlaylistGenreView() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string }>();
|
||||
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
|
||||
const { access_token, isLoggedIn } = useContext(authContext);
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
|
||||
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 { data: playlists } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
||||
[QueryCacheKeys.genrePlaylists, id],
|
||||
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id)
|
||||
.then((playlistsRes) => playlistsRes.body.playlists.items),
|
||||
{ initialData: [] }
|
||||
)
|
||||
|
||||
const playlistGenreViewStylesheet = `
|
||||
#genre-container{
|
||||
@ -56,9 +45,15 @@ function PlaylistGenreView() {
|
||||
<Text id="heading">{`<h2>${location.state.name}</h2>`}</Text>
|
||||
<ScrollArea id="scroll-view">
|
||||
<View id="child-container">
|
||||
{isLoggedIn &&
|
||||
playlists.map((playlist, index) => {
|
||||
return <PlaylistCard key={((index * Date.now()) / Math.random()) * 100} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
|
||||
{playlists?.map((playlist, index) => {
|
||||
return (
|
||||
<PlaylistCard
|
||||
key={index + playlist.id}
|
||||
id={playlist.id}
|
||||
name={playlist.name}
|
||||
thumbnail={playlist.images[0].url}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { useLocation, useParams } from "react-router";
|
||||
import { Direction, QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
|
||||
@ -44,15 +44,15 @@ const PlaylistView: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BoxView direction={Direction.TopToBottom}>
|
||||
<BoxView style={`max-width: 150px;`}>
|
||||
<View style={`flex: 1; flex-direction: 'column'; flex-grow: 1;`}>
|
||||
<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(currentPlaylist?.id === params.id ? stop : play)} />
|
||||
</BoxView>
|
||||
</View>
|
||||
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
|
||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||
<BoxView /* style={`flex-direction:column;`} */ direction={Direction.TopToBottom}>
|
||||
<ScrollArea style={`flx:1; flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column; flex: 1;`}>
|
||||
{isLoading && <Text>{`Loading Tracks...`}</Text>}
|
||||
{isError && (
|
||||
<>
|
||||
@ -78,9 +78,9 @@ const PlaylistView: FC = () => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</BoxView>
|
||||
</View>
|
||||
</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{
|
||||
categories="categories",
|
||||
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 { CredentialKeys } from "../app";
|
||||
import authContext from "../context/authContext";
|
||||
import showError from "../helpers/showError";
|
||||
import spotifyApi from "../initializations/spotifyApi";
|
||||
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
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.setClientSecret(clientSecret);
|
||||
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);
|
||||
console.log(chalk.bgCyan.green("Finished setting up credentials"));
|
||||
}
|
||||
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 useSpotifyApi from "./useSpotifyApi";
|
||||
import useSpotifyApiError from "./useSpotifyApiError";
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React, { useContext } from "react";
|
||||
import {View} from "@nodegui/react-nodegui";
|
||||
import { Route } from "react-router";
|
||||
import authContext from "./context/authContext";
|
||||
import Home from "./components/Home";
|
||||
import Login from "./components/Login";
|
||||
import PlaylistView from "./components/PlaylistView";
|
||||
import PlaylistGenreView from "./components/PlaylistGenreView";
|
||||
import TabMenu from "./components/TabMenu";
|
||||
|
||||
function Routes() {
|
||||
const {
|
||||
@ -12,11 +14,18 @@ function Routes() {
|
||||
} = useContext(authContext);
|
||||
return (
|
||||
<>
|
||||
<Route exact path="/">
|
||||
{isLoggedIn ? <Home /> : <Login />}
|
||||
<Route path="/">
|
||||
{ isLoggedIn ?
|
||||
<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="/genre/playlists/:id"><PlaylistGenreView /></Route>
|
||||
</View>
|
||||
: <Login/>
|
||||
}
|
||||
|
||||
</Route>
|
||||
<Route exact path="/playlist/:id"><PlaylistView/></Route>
|
||||
<Route exact path="/genre/playlists/:id"><PlaylistGenreView/></Route>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user