From 078d780552e1086a0b7843d3cf33552c69642c55 Mon Sep 17 00:00:00 2001 From: eirki Date: Sat, 30 Jul 2022 21:14:13 +0200 Subject: [PATCH] Add context menu --- backend/api.py | 9 +++ backend/main.py | 59 ++++++++++++++- backend/version.py | 2 +- frontend/package-lock.json | 28 +++++++- frontend/package.json | 4 +- frontend/src/components/Album.vue | 96 ++++++++++++++++++++++--- frontend/src/components/AlbumColumn.vue | 11 ++- frontend/src/components/AlbumGrid.vue | 13 +++- frontend/src/components/Home.vue | 70 ++++++++++++++++-- frontend/src/components/Spinner.vue | 52 ++++++++++++++ frontend/src/main.ts | 18 ++++- frontend/src/types.ts | 6 ++ 12 files changed, 342 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/Spinner.vue diff --git a/backend/api.py b/backend/api.py index 93d9b0c..db30be9 100644 --- a/backend/api.py +++ b/backend/api.py @@ -15,6 +15,7 @@ scope = [ "user-library-read", + "user-library-modify", "user-read-playback-state", "user-modify-playback-state", "user-read-currently-playing", @@ -115,6 +116,14 @@ def albums(spotify: Spotify) -> dict[str, list[dict]]: return data +def add_album(spotify: Spotify, album_id: str) -> None: + spotify.current_user_saved_albums_add([album_id]) + + +def remove_album(spotify: Spotify, album_id: str) -> None: + spotify.current_user_saved_albums_delete([album_id]) + + class AlbumResponse(TypedDict): added_at: str # "2021-05-13T07:00:09Z" album: Album diff --git a/backend/main.py b/backend/main.py index 6051d92..563071e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,7 +2,7 @@ import sys from typing import Optional -from fastapi import Cookie, FastAPI, Request +from fastapi import Cookie, FastAPI, HTTPException, Request from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -49,7 +49,64 @@ async def root(request: Request, spotify_token: Optional[str] = Cookie(None)): "index.html", {"request": request, "data": albums}, ) + if cache: + set_token_cookie(response, cache) + return response + + +@app.get("/albums") +async def albums(spotify_token: Optional[str] = Cookie(None)): + spotify_token_j = json.loads(spotify_token) if spotify_token else None + authed, auth_manager, cache = api.check_auth(spotify_token_j) + if not authed: + return RedirectResponse(auth_manager.get_authorize_url()) + spotify = Spotify(auth_manager=auth_manager) + albums = api.albums(spotify) + response = JSONResponse(content=albums) + if cache: + set_token_cookie(response, cache) + cache.delete_cached_token() + return response + + +@app.get("/add_album/{album_id}") +async def add_album(album_id: str, spotify_token: Optional[str] = Cookie(None)): + spotify_token_j = json.loads(spotify_token) if spotify_token else None + authed, auth_manager, cache = api.check_auth(spotify_token_j) + if not authed: + raise HTTPException(status_code=403, detail="Authentication failed") + spotify = Spotify(auth_manager=auth_manager) + api.add_album(spotify, album_id) + response = JSONResponse(content={"success": True}) if cache: set_token_cookie(response, cache) cache.delete_cached_token() return response + + +@app.get("/remove_album/{album_id}") +async def remove_album(album_id: str, spotify_token: Optional[str] = Cookie(None)): + spotify_token_j = json.loads(spotify_token) if spotify_token else None + authed, auth_manager, cache = api.check_auth(spotify_token_j) + if not authed: + raise HTTPException(status_code=403, detail="Authentication failed") + spotify = Spotify(auth_manager=auth_manager) + api.remove_album(spotify, album_id) + response = JSONResponse(content={"success": True}) + if cache: + set_token_cookie(response, cache) + cache.delete_cached_token() + return response + + +@app.get("/authed") +async def authed(spotify_token: Optional[str] = Cookie(None)): + spotify_token_j = json.loads(spotify_token) if spotify_token else None + authed, auth_manager, cache = api.check_auth(spotify_token_j) + success = authed is not None + res = {"success": success} + response = JSONResponse(content=res) + if cache: + set_token_cookie(response, cache) + cache.delete_cached_token() + return res diff --git a/backend/version.py b/backend/version.py index 22385e8..ef159bc 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1 +1 @@ -version = "22.07.29.0" +version = "22.07.30.0" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3c20042..290e117 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,9 @@ "name": "frontend2", "version": "0.0.0", "dependencies": { - "vue": "^3.2.37" + "@imengyu/vue3-context-menu": "^1.0.9", + "vue": "^3.2.37", + "vue-toastification": "^2.0.0-rc.5" }, "devDependencies": { "@vitejs/plugin-vue": "^3.0.0", @@ -28,6 +30,11 @@ "node": ">=6.0.0" } }, + "node_modules/@imengyu/vue3-context-menu": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@imengyu/vue3-context-menu/-/vue3-context-menu-1.0.9.tgz", + "integrity": "sha512-ayK8Tmmg0Pgwc3l1UbMP7c23XQ9YzfKiCBFRUtJbIuDqLj6WtYowTTWSLj+NNc9bushs9KWkyAyuBKGiQBEHeQ==" + }, "node_modules/@vitejs/plugin-vue": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.0.1.tgz", @@ -777,6 +784,14 @@ "@vue/shared": "3.2.37" } }, + "node_modules/vue-toastification": { + "version": "2.0.0-rc.5", + "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz", + "integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==", + "peerDependencies": { + "vue": "^3.0.2" + } + }, "node_modules/vue-tsc": { "version": "0.38.8", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-0.38.8.tgz", @@ -799,6 +814,11 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz", "integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==" }, + "@imengyu/vue3-context-menu": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@imengyu/vue3-context-menu/-/vue3-context-menu-1.0.9.tgz", + "integrity": "sha512-ayK8Tmmg0Pgwc3l1UbMP7c23XQ9YzfKiCBFRUtJbIuDqLj6WtYowTTWSLj+NNc9bushs9KWkyAyuBKGiQBEHeQ==" + }, "@vitejs/plugin-vue": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.0.1.tgz", @@ -1261,6 +1281,12 @@ "@vue/shared": "3.2.37" } }, + "vue-toastification": { + "version": "2.0.0-rc.5", + "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz", + "integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==", + "requires": {} + }, "vue-tsc": { "version": "0.38.8", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-0.38.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index 262beee..2208cc3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,9 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.2.37" + "@imengyu/vue3-context-menu": "^1.0.9", + "vue": "^3.2.37", + "vue-toastification": "^2.0.0-rc.5" }, "devDependencies": { "@vitejs/plugin-vue": "^3.0.0", diff --git a/frontend/src/components/Album.vue b/frontend/src/components/Album.vue index 7f6d4e9..1424b3b 100644 --- a/frontend/src/components/Album.vue +++ b/frontend/src/components/Album.vue @@ -3,21 +3,18 @@ padding: `${padding}px` }" @click="play"> + @mouseleave="clearHover" @contextmenu="onContextMenu($event)" /> diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 01433bc..66f9ac1 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,4 +1,20 @@ +import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css' +import ContextMenu from '@imengyu/vue3-context-menu' + +import Toast, { PluginOptions, POSITION } from "vue-toastification"; +import "vue-toastification/dist/index.css"; + import { createApp } from 'vue' import App from './App.vue' -createApp(App).mount('#app') +const ToastOptions: PluginOptions = { + position: POSITION.BOTTOM_LEFT, + icon: false, + showCloseButtonOnHover: true, + hideProgressBar: true, + toastClassName: "custom-toast", + transition: "", + timeout: 1000, + maxToasts: 1, +}; +createApp(App).use(ContextMenu).use(Toast, ToastOptions).mount('#app') diff --git a/frontend/src/types.ts b/frontend/src/types.ts index eb4913c..f9b209b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,10 +1,16 @@ export type ImageT = { url: string } +interface ArtistT { + name: string + id: string +} + export interface AlbumT { name: string id: string uri: string images: [ImageT, ImageT, ImageT] + artists: ArtistT[] } export interface OverlayT {