diff --git a/src/backend/state/theme-store.ts b/src/backend/state/theme-store.ts index 7143ceb..5a6c92f 100644 --- a/src/backend/state/theme-store.ts +++ b/src/backend/state/theme-store.ts @@ -51,6 +51,8 @@ export interface CSSLoaderStateActions { request?: RequestInit, requiresAuth?: boolean | string ) => Promise; + logInWithShortToken: (newToken?: string) => Promise; + logOut: () => void; getThemes: () => Promise; changePreset: (presetName: string) => Promise; testBackend: () => Promise; @@ -171,6 +173,10 @@ export const createCSSLoaderStore = (backend: Backend) => const hiddenMotd = await backend.storeRead("hiddenMotd"); set({ hiddenMotdId: hiddenMotd ?? "" }); + if (shortToken) { + await get().logInWithShortToken(); + } + const { bulkThemeUpdateCheck, scheduleBulkThemeUpdateCheck } = get(); await bulkThemeUpdateCheck(); scheduleBulkThemeUpdateCheck(); @@ -212,6 +218,50 @@ export const createCSSLoaderStore = (backend: Backend) => console.error("Error Reloading Themes", error); } }, + logInWithShortToken: async (newToken?: string) => { + try { + const token = newToken ?? get().apiShortToken; + if (!token) { + throw new Error("No Token Provided"); + } + // This can't use apiFetch because it doesn't use header based auth + const json = await backend.fetch<{ token: string }>(`${apiUrl}/auth/authenticate_token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + if (!json.token) { + throw new FetchError( + "Token Authentication Failed", + `${apiUrl}/auth/authenticate_token`, + "No Token in Response" + ); + } + backend.storeWrite("shortToken", token); + set({ + apiShortToken: token, + apiFullToken: json.token, + apiTokenExpireDate: new Date().valueOf() + 1000 * 10 * 60, + }); + const meJson = await apiFetch("/auth/me", undefined, true); + if (meJson) { + set({ apiMeData: meJson }); + } + } catch (error) { + backend.toast("CSSLoader", "Failed to log in"); + } + }, + logOut: () => { + set({ + apiShortToken: "", + apiFullToken: "", + apiMeData: undefined, + apiTokenExpireDate: undefined, + }); + backend.storeWrite("shortToken", ""); + }, refreshToken: async (): Promise => { const { apiFullToken, apiTokenExpireDate } = get(); if (!apiFullToken) { diff --git a/src/modules/theme-store/pages/AccountPage.tsx b/src/modules/theme-store/pages/AccountPage.tsx new file mode 100644 index 0000000..58feca7 --- /dev/null +++ b/src/modules/theme-store/pages/AccountPage.tsx @@ -0,0 +1,69 @@ +import { useCSSLoaderAction, useCSSLoaderValue } from "@/backend"; +import { DialogButton, Focusable, TextField } from "@decky/ui"; +import { useState } from "react"; +import { FaArrowRightToBracket } from "react-icons/fa6"; + +export function AccountPage() { + const apiFullToken = useCSSLoaderValue("apiFullToken"); + + return ( +
+

{apiFullToken ? "Your Account" : "Log In"}

+ {apiFullToken ? : } +

+ Logging in gives you access to star themes, saving them to their own page where you can + quickly find them. +
+ To get started, create an account on deckthemes.com and generate an account key from your + profile page. +
+

+
+ ); +} +function LoggedInSection() { + const apiMeData = useCSSLoaderValue("apiMeData"); + const logOut = useCSSLoaderAction("logOut"); + return ( + + + {apiMeData ? `Logged in as ${apiMeData.username}` : "Loading..."} + + + Unlink My Deck + + + ); +} + +function LoggedOutSection() { + const apiFullToken = useCSSLoaderValue("apiFullToken"); + const logInWithShortToken = useCSSLoaderAction("logInWithShortToken"); + const apiShortToken = useCSSLoaderValue("apiShortToken"); + + const [shortTokenInterimValue, setShortTokenIntValue] = useState(apiShortToken); + + return ( + +
+ setShortTokenIntValue(e.target.value)} + /> +
+ { + logInWithShortToken(shortTokenInterimValue); + }} + > + + Log In + +
+ ); +} diff --git a/src/modules/theme-store/pages/ThemeStoreRouter.tsx b/src/modules/theme-store/pages/ThemeStoreRouter.tsx index 76c0af6..96fde7d 100644 --- a/src/modules/theme-store/pages/ThemeStoreRouter.tsx +++ b/src/modules/theme-store/pages/ThemeStoreRouter.tsx @@ -5,49 +5,90 @@ import { useThemeBrowserSharedAction, useThemeBrowserSharedValue, } from "../context"; +import { AccountPage } from "./AccountPage"; +import { useCSSLoaderValue } from "@/backend"; +import { Permissions } from "@/types"; // TODO: Make the tab definition a constant so that it isn't re-generated every page load export function ThemeStoreRouter() { const currentTab = useThemeBrowserSharedValue("currentTab"); const setCurrentTab = useThemeBrowserSharedAction("setCurrentTab"); + + const apiMeData = useCSSLoaderValue("apiMeData"); + + const tabs = [ + { + id: "bpm-themes", + title: "Deck UI Themes", + content: ( + + + + ), + }, + { + id: "desktop-themes", + title: "Desktop Themes", + content: ( + + + + ), + }, + { + id: "account", + title: "Account", + content: , + }, + ]; + + apiMeData?.permissions?.includes(Permissions.viewSubs) && + tabs.splice(2, 0, { + id: "submissions", + title: "Submissions", + content: ( + + + + ), + }); + + apiMeData?.username && + tabs.splice(2, 0, { + id: "starred-themes", + title: "Starred Themes", + content: ( + + + + ), + }); + return (
- setCurrentTab(tab)} - tabs={[ - { - id: "bpm-themes", - title: "BPM Themes", - content: ( - - - - ), - }, - { - id: "desktop-themes", - title: "Desktop Themes", - content: ( - - - - ), - }, - ]} - > + setCurrentTab(tab)} tabs={tabs}>
); } diff --git a/src/styles/styles-as-string.ts b/src/styles/styles-as-string.ts index 52f6475..e6549d5 100644 --- a/src/styles/styles-as-string.ts +++ b/src/styles/styles-as-string.ts @@ -6,6 +6,10 @@ export const styles = ` /* THAT IS NEEDED FOR STATIC CLASS INJECTION */ /* LINT ERRORS ARE TO BE EXPECTED, BECAUSE THIS USES TEMPLATE LITERALS THAT WILL BE FILLED IN BY styles-as-string.ts */ +/* +MARK: TAILWIND +*/ + .flex { display: flex !important; } @@ -18,6 +22,10 @@ export const styles = ` flex-wrap: wrap !important; } +.flex-1 { + flex: 1 1 0% !important; +} + .gap-1 { gap: 0.25rem !important; } @@ -93,7 +101,9 @@ export const styles = ` transform: translate(-50%, -50%) !important; } -/* Fullscreen Routes */ +/* +MARK: Fullscreen Routes +*/ .cl_fullscreenroute_container { margin-top: 40px !important; @@ -101,7 +111,15 @@ export const styles = ` background: #0e141b !important; } -/* TitleView */ +.cl_fullscrenroute_title { + font-size: 2rem !important; + font-weight: bold !important; +} + +/* +MARK: TitleView +*/ + .cl-title-view-button { height: 28px !important; @@ -131,7 +149,10 @@ export const styles = ` animation: onboardingButton 1s infinite ease-in-out !important; } -/* QAM Tab */ +/* +MARK: QAM Tab +*/ + .cl-qam-collapse-button-container > div > div > div > div > button { height: 10px !important; @@ -190,7 +211,10 @@ export const styles = ` white-space: nowrap !important; } -/* Theme Store */ +/* +MARK: Store +*/ + .cl-store-filter-field-container { display: flex !important; @@ -249,6 +273,7 @@ export const styles = ` /* The variables should be injected wherever needed */ /* This module actually is based on font-size, so EM makes sense over REM */ +/* TODO: For some reason I made half of these classes with dashes and the other half with underscores, standardize it!!! */ .cl_storeitem_notifbubble { position: absolute !important; background: linear-gradient(135deg, #fca904 50%, transparent 51%) !important; @@ -342,7 +367,10 @@ export const styles = ` font-size: 0.75em !important; } -/* Expanded View */ +/* +MARK: Expanded View +*/ + @keyframes cl_spin { to { @@ -505,4 +533,16 @@ export const styles = ` min-width: 1rem !important; position: relative; } + +/* +MARK: Account Page +*/ + +.cl_accountpage_actionbutton { + max-width: 30% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 0.5rem !important; +} `; diff --git a/src/styles/styles.css b/src/styles/styles.css index af464cf..00ab4b5 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -3,6 +3,10 @@ /* THAT IS NEEDED FOR STATIC CLASS INJECTION */ /* LINT ERRORS ARE TO BE EXPECTED, BECAUSE THIS USES TEMPLATE LITERALS THAT WILL BE FILLED IN BY styles-as-string.ts */ +/* +MARK: TAILWIND +*/ + .flex { display: flex !important; } @@ -15,6 +19,10 @@ flex-wrap: wrap !important; } +.flex-1 { + flex: 1 1 0% !important; +} + .gap-1 { gap: 0.25rem !important; } @@ -90,7 +98,9 @@ transform: translate(-50%, -50%) !important; } -/* Fullscreen Routes */ +/* +MARK: Fullscreen Routes +*/ .cl_fullscreenroute_container { margin-top: 40px !important; @@ -98,7 +108,15 @@ background: #0e141b !important; } -/* TitleView */ +.cl_fullscrenroute_title { + font-size: 2rem !important; + font-weight: bold !important; +} + +/* +MARK: TitleView +*/ + .cl-title-view-button { height: 28px !important; @@ -128,7 +146,10 @@ animation: onboardingButton 1s infinite ease-in-out !important; } -/* QAM Tab */ +/* +MARK: QAM Tab +*/ + .cl-qam-collapse-button-container > div > div > div > div > button { height: 10px !important; @@ -187,7 +208,10 @@ white-space: nowrap !important; } -/* Theme Store */ +/* +MARK: Store +*/ + .cl-store-filter-field-container { display: flex !important; @@ -246,6 +270,7 @@ /* The variables should be injected wherever needed */ /* This module actually is based on font-size, so EM makes sense over REM */ +/* TODO: For some reason I made half of these classes with dashes and the other half with underscores, standardize it!!! */ .cl_storeitem_notifbubble { position: absolute !important; background: linear-gradient(135deg, #fca904 50%, transparent 51%) !important; @@ -339,7 +364,10 @@ font-size: 0.75em !important; } -/* Expanded View */ +/* +MARK: Expanded View +*/ + @keyframes cl_spin { to { @@ -501,4 +529,16 @@ width: 1rem !important; min-width: 1rem !important; position: relative; +} + +/* +MARK: Account Page +*/ + +.cl_accountpage_actionbutton { + max-width: 30% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 0.5rem !important; } \ No newline at end of file