Skip to content

Commit

Permalink
WIP: display uploaded profiles in settings
Browse files Browse the repository at this point in the history
  • Loading branch information
beebls committed Jan 22, 2024
1 parent 96a3163 commit 63925c6
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 11 deletions.
3 changes: 2 additions & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function logInWithShortToken(
setGlobalState("apiShortToken", shortTokenValue);
setGlobalState("apiFullToken", data.token);
setGlobalState("apiTokenExpireDate", new Date().valueOf() + 1000 * 60 * 10);
genericGET(`/auth/me`, true, data.token).then((meData) => {
genericGET(`/auth/me_full`, true, data.token).then((meData) => {
if (meData?.username) {
setGlobalState("apiMeData", meData);
toast("Logged In!", `Logged in as ${meData.username}`);
Expand All @@ -70,6 +70,7 @@ export async function logInWithShortToken(
})
.catch((err) => {
console.error(`Error authenticating from short token.`, err);
toast("Error Authenticating", JSON.stringify(err));
});
} else {
toast("Invalid Token", "Token must be 12 characters long.");
Expand Down
9 changes: 9 additions & 0 deletions src/apiTypes/AccountData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export interface AccountData extends UserInfo {
permissions: Permissions[];
}

export type PremiumTiers = "None" | "Tier1" | "Tier2" | "Tier3";

export interface FullAccountData extends AccountData {
hasDeckKey: boolean;
premiumTier: PremiumTiers;
email: string;
lastLoginDate: string;
}

export interface AuthContextContents {
accountInfo: AccountData | undefined;
setAccountInfo:
Expand Down
15 changes: 15 additions & 0 deletions src/components/Modals/PremiumFeatureModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Focusable, ModalRoot } from "decky-frontend-lib";

export function PremiumFeatureModal({ closeModal, blurb }: { closeModal?: any; blurb: string }) {
return (
<ModalRoot onCancel={closeModal} onEscKeypress={closeModal}>
<Focusable style={{ display: "flex", flexDirection: "column" }}>
<span style={{ fontSize: "2em", fontWeight: "bold" }}>Premium Feature</span>
<p>{blurb}</p>
<span>
To support DeckThemes and unlock premium features, visit https://patreon.com/deckthemes
</span>
</Focusable>
</ModalRoot>
);
}
63 changes: 63 additions & 0 deletions src/components/Modals/UploadProfileModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { CssLoaderContextProvider, useCssLoaderState } from "../../state";
import * as python from "../../python";
import {
ButtonItem,
ConfirmModal,
DropdownItem,
Focusable,
ModalRoot,
TextField,
ToggleField,
} from "decky-frontend-lib";
import { useMemo, useState } from "react";
import { Flags } from "../../ThemeTypes";

export function UploadProfileModalRoot({ closeModal }: { closeModal?: any }) {
return (
<ModalRoot onCancel={closeModal} onEscKeypress={closeModal}>
{/* @ts-ignore */}
<CssLoaderContextProvider cssLoaderStateClass={python.globalState}>
<UploadProfileModal />
</CssLoaderContextProvider>
</ModalRoot>
);
}

function UploadProfileModal() {
const { localThemeList } = useCssLoaderState();
let [selectedProfile, setProfile] = useState(
localThemeList.find((e) => e.flags.includes(Flags.isPreset))?.id
);
const profiles = useMemo(() => {
return localThemeList.filter((e) => e.flags.includes(Flags.isPreset));
}, [localThemeList]);
const eligibleProfiles = useMemo(() => {
return profiles;
}, [profiles]);

const [isPublic, setPublic] = useState<boolean>(false);
const [description, setDescription] = useState<string>("");

return (
<Focusable style={{ display: "flex", flexDirection: "column" }}>
<span>Upload Profile</span>
<DropdownItem
selectedOption={selectedProfile}
rgOptions={eligibleProfiles.map((e) => ({ data: e.id, label: e.display_name }))}
onChange={(chosen) => {
setProfile(chosen.data);
}}
label="Profile To Upload"
/>
<ToggleField checked={isPublic} onChange={setPublic} label="Make Profile Public" />
<TextField
label="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<ButtonItem>
<span>Upload</span>
</ButtonItem>
</Focusable>
);
}
1 change: 1 addition & 0 deletions src/components/Modals/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./CreatePresetModal";
export * from "./ThemeSettingsModal";
export * from "./UploadProfileModal";
134 changes: 129 additions & 5 deletions src/pages/settings/PresetSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { Focusable, PanelSection } from "decky-frontend-lib";
import { ButtonItem, DialogButton, Focusable, PanelSection, showModal } from "decky-frontend-lib";
import { useCssLoaderState } from "../../state";
import { Flags, Theme } from "../../ThemeTypes";
import { useState } from "react";
import { PresetSelectionDropdown } from "../../components";
import { useEffect, useState } from "react";
import {
PresetSelectionDropdown,
UploadProfileModalRoot,
VariableSizeCard,
} from "../../components";
import { FullscreenProfileEntry } from "../../components/ThemeSettings/FullscreenProfileEntry";
import { installTheme } from "../../api";
import { genericGET, installTheme, logInWithShortToken } from "../../api";
import * as python from "../../python";
import { PartialCSSThemeInfo, ThemeQueryResponse } from "../../apiTypes";
import { ThemeBrowserCardStyles } from "../../components/Styles";
import { PremiumFeatureModal } from "../../components/Modals/PremiumFeatureModal";

export function PresetSettings() {
const { localThemeList, setGlobalState, updateStatuses } = useCssLoaderState();
const { localThemeList, setGlobalState, updateStatuses, apiShortToken, apiFullToken, apiMeData } =
useCssLoaderState();

const [isInstalling, setInstalling] = useState(false);

Expand Down Expand Up @@ -47,6 +55,122 @@ export function PresetSettings() {
))}
</Focusable>
</PanelSection>
<PanelSection title="Your Uploaded Profiles">
<UploadedProfilesDisplay />
</PanelSection>
</div>
);
}

function UploadedProfilesDisplay() {
const { apiFullToken, apiShortToken, apiMeData } = useCssLoaderState();

const [publicProfiles, setPublicProfiles] = useState<PartialCSSThemeInfo[]>([]);
const [privateProfiles, setPrivateProfiles] = useState<PartialCSSThemeInfo[]>([]);
const [profilesLoaded, setLoaded] = useState<boolean>(false);
useEffect(() => {
async function getUserProfiles() {
if (!apiFullToken) {
await logInWithShortToken();
}

// Since the short token could be invalid, we still have to re-check for if the log in actually worked.
// The react value doesn't update mid function, so we re-grab it.
const upToDateFullToken = python.globalState?.getGlobalState("apiFullToken");
console.log("up to date token", upToDateFullToken);
if (!upToDateFullToken) return;
const publicProfileData = await genericGET("/users/me/themes?filters=", true);
if (publicProfileData && publicProfileData.total > 0) {
setPublicProfiles(publicProfileData.items);
}
const privateProfileData = await genericGET("/users/me/themes/private?filters=", true);
if (privateProfileData && privateProfileData.total > 0) {
setPrivateProfiles(privateProfileData.items);
}
setLoaded(true);
}
if (apiShortToken) getUserProfiles();
}, []);

if (!apiMeData) {
return (
<>
{apiShortToken ? (
<>
<span>Loading</span>
</>
) : (
<>
<span>
You are not logged in. Log In with your DeckThemes account to view your uploaded
profiles.
</span>
</>
)}
</>
);
}

return (
<>
<ThemeBrowserCardStyles customCardSize={5} />
<Focusable style={{ display: "flex", flexDirection: "column", position: "relative" }}>
<DialogButton
style={{
width: "fit-content",
opacity: apiMeData.premiumTier && apiMeData.premiumTier !== "None" ? "100%" : "50%",
position: "absolute",
top: "-2.6em",
right: "0",
}}
onClick={() => {
if (apiMeData.premiumTier && apiMeData.premiumTier !== "None") {
showModal(<UploadProfileModalRoot />);
return;
}
showModal(
<PremiumFeatureModal blurb="Since syncing profiles from your Deck to DeckThemes servers uses up storage, this feature is for those who support us on Patreon and help pay the bills." />
);
return;
}}
>
Upload Profile
</DialogButton>
{profilesLoaded ? (
<>
<Focusable style={{ display: "flex", flexDirection: "column" }}>
{publicProfiles.length > 0 && (
<>
<Focusable style={{ display: "flex", flexDirection: "column" }}>
<span>Public Profiles:</span>
<Focusable style={{ display: "flex", flexWrap: "wrap", gap: "1em" }}>
{publicProfiles.map((e) => (
<VariableSizeCard data={e} cols={5} onClick={() => {}} />
))}
</Focusable>
</Focusable>
</>
)}
{apiMeData.premiumTier &&
apiMeData.premiumTier !== "None" &&
privateProfiles.length > 0 ? (
<>
<Focusable style={{ display: "flex", flexDirection: "column" }}>
<span>Private Profiles:</span>
<Focusable style={{ display: "flex", flexWrap: "wrap", gap: "1em" }}>
{privateProfiles.map((e) => (
<VariableSizeCard data={e} cols={5} onClick={() => {}} />
))}
</Focusable>
</Focusable>
</>
) : null}
</Focusable>
</>
) : (
<span>Loading Profiles...</span>
)}
</Focusable>
</>
);
}
3 changes: 2 additions & 1 deletion src/pages/settings/SettingsPageRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { ThemeSettings } from "./ThemeSettings";
import { PresetSettings } from "./PresetSettings";
import { PluginSettings } from "./PluginSettings";
import { Credits } from "./Credits";
import { AiFillGithub, AiFillHeart } from "react-icons/ai";
import { DonatePage } from "./DonatePage";
import { FaFolder, FaGithub, FaHeart } from "react-icons/fa";
import { ThemeBrowserCardStyles } from "../../components/Styles";

export function SettingsPageRouter() {
return (
<>
<ThemeBrowserCardStyles />
<style>
{`
/* Remove side padding on the PanelSections */
Expand Down
2 changes: 0 additions & 2 deletions src/pages/theme-manager/ThemeManagerRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { LogInPage } from "./LogInPage";
import { StarredThemesPage } from "./StarredThemesPage";
import { SubmissionsPage } from "./SubmissionBrowserPage";
import { ThemeBrowserPage } from "./ThemeBrowserPage";
import { ThemeBrowserCardStyles } from "../../components/Styles";
export function ThemeManagerRouter() {
const { apiMeData, currentTab, setGlobalState, browserCardSize } = useCssLoaderState();
return (
Expand All @@ -16,7 +15,6 @@ export function ThemeManagerRouter() {
background: "#0e141b",
}}
>
<ThemeBrowserCardStyles />
<Tabs
activeTab={currentTab}
onShowTab={(tabID: string) => {
Expand Down
5 changes: 3 additions & 2 deletions src/state/CssLoaderState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createContext, FC, useContext, useEffect, useState } from "react";
import {
AccountData,
FilterQueryResponse,
FullAccountData,
PartialCSSThemeInfo,
ThemeQueryRequest,
ThemeQueryResponse,
Expand Down Expand Up @@ -37,7 +38,7 @@ interface PublicCssLoaderState {
apiShortToken: string;
apiFullToken: string;
apiTokenExpireDate: Date | number | undefined;
apiMeData: AccountData | undefined;
apiMeData: FullAccountData | undefined;
// This is a unix timestamp
nextUpdateCheckTime: number;
updateCheckTimeout: NodeJS.Timeout | undefined;
Expand Down Expand Up @@ -75,7 +76,7 @@ export class CssLoaderState {
private apiShortToken: string = "";
private apiFullToken: string = "";
private apiTokenExpireDate: Date | number | undefined = undefined;
private apiMeData: AccountData | undefined = undefined;
private apiMeData: FullAccountData | undefined = undefined;
private localThemeList: Theme[] = [];
private themeErrors: ThemeError[] = [];
private selectedRepo: SingleDropdownOption = {
Expand Down

0 comments on commit 63925c6

Please sign in to comment.