Skip to content

Commit

Permalink
WIP: add more profile upload stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
beebls committed Feb 27, 2024
1 parent 60f5538 commit 3b54ec7
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 10 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
},
"dependencies": {
"color": "^4.2.3",
"decky-frontend-lib": "^3.24.3",
"decky-frontend-lib": "^3.24.5",
"lodash": "^4.17.21",
"react-icons": "^4.12.0"
},
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/apiTypes/SubmissionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface TaskQueryResponse {
id: string;
name: string;
status: string;
completed: Date;
started: Date;
success: boolean;
}

export interface ZipSubmitRequest {
blob: string;
description: string;
privateSubmission: boolean;
imageBlobs: string[];
target?: string;
}
2 changes: 2 additions & 0 deletions src/apiTypes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./CSSThemeTypes";
export * from "./AccountData";
export * from "./BlobTypes";
export * from "./Motd";
export * from "./SubmissionTypes";
100 changes: 100 additions & 0 deletions src/backend/apiHelpers/fetchWrappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { refreshToken } from "../../api";
import { toast } from "../../python";
import { globalState, server } from "../pythonRoot";

function createHeadersObj(authToken: string | undefined, request: RequestInit | undefined) {
const headers = new Headers();
if (request && request.headers) {
for (const [key, value] of Object.entries(request.headers)) {
headers.append(key, value);
}
}
if (authToken) headers.set("Authorization", `Bearer ${authToken}`);

return headers;
}

export async function genericApiFetch(
fetchPath: string,
request: RequestInit | undefined = undefined,
options: {
requiresAuth?: boolean;
onError?: () => void;
customAuthToken?: string;
failSilently?: boolean;
} = {
requiresAuth: false,
customAuthToken: undefined,
onError: () => {},
failSilently: false,
}
) {
const {
requiresAuth = false,
customAuthToken = undefined,
onError = () => {},
failSilently = false,
} = options;

const { apiUrl } = globalState!.getPublicState();
function doTheFetching(authToken: string | undefined = undefined) {
const headers = createHeadersObj(authToken, request);
console.log("TOKEN", authToken);

console.log("TEST", {
method: "GET",
// If a custom method is specified in request it will overwrite
...request,
headers: headers,
});
return server!
.fetchNoCors<Response>(`${apiUrl}${fetchPath}`, {
method: "GET",
// If a custom method is specified in request it will overwrite
...request,
headers: headers,
})
.then((deckyRes) => {
if (deckyRes.success) {
return deckyRes.result;
}
throw new Error(`Fetch not successful!`);
})
.then((res) => {
if (res.status >= 200 && res.status <= 300 && res.body) {
// @ts-ignore
return JSON.parse(res.body || "");
}
throw new Error(`Res not OK!, code ${res.status} - ${res.body}`);
})
.then((json) => {
console.log("JSON", json);

if (json) {
return json;
}
throw new Error(`No json returned!`);
})
.catch((err) => {
if (!failSilently) {
console.error(`Error fetching ${fetchPath}`, err);
}
onError();
});
}
if (requiresAuth) {
if (customAuthToken) {
return doTheFetching(customAuthToken);
}
return refreshToken(onError).then((token) => {
if (token) {
return doTheFetching(token);
} else {
toast("Error Refreshing Token!", "");
return;
}
});
} else {
return doTheFetching();
}
}
2 changes: 2 additions & 0 deletions src/backend/apiHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./fetchWrappers";
export * from "./fetchWrappers";
74 changes: 74 additions & 0 deletions src/backend/apiHelpers/profileUploadingHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { TaskQueryResponse } from "../../apiTypes/SubmissionTypes";
import { genericApiFetch } from "./fetchWrappers";

async function fetchLocalZip(fileName: string) {
if (!fileName.endsWith(".zip")) {
throw new Error(`File must be a .zip!`);
}
const filesRes = await fetch(`/themes_custom/${fileName}`);
if (!filesRes.ok) {
throw new Error(`Couldn't fetch zip!`);
}
const rawBlob = await filesRes.blob();
const correctBlob = new Blob([rawBlob], { type: "application/x-zip-compressed" });
return correctBlob;
}

export async function uploadZipAsBlob(fileName: string): Promise<string> {
if (!fileName.endsWith(".zip")) {
throw new Error(`File must be a .zip!`);
}
const fileBlob = await fetchLocalZip(fileName);
console.log("BLOB", fileBlob);
const formData = new FormData();
formData.append("File", fileBlob, fileName);
console.log("FORM", formData);

const json = await genericApiFetch(
"/blobs",
{
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
},
body: formData,
},
{ requiresAuth: true }
);
if (json) {
return json;
}
throw new Error(`No json returned!`);
}

export async function publishProfile(
profileId: string,
isPublic: boolean,
description: string
): Promise<string> {
// const zipName = `${profileId}.zip`;
const zipName = "round.zip";
const blobId = await uploadZipAsBlob(zipName);
if (!blobId) throw new Error(`No blobId returned!`);

// ALL OF THIS IS UNTESTED, BLOB IS 415'ing RN
const json = await genericApiFetch(
`/submissions/css_zip`,
{
method: "POST",
body: JSON.stringify({
blob: blobId,
imageBlobs: [],
description: description,
privateSubmission: !isPublic,
}),
},
{ requiresAuth: true }
);
if (!json || !json.task) throw new Error(`No task returned`);
return json.task;
}

export async function getTaskStatus(taskId: string): Promise<TaskQueryResponse> {
return await genericApiFetch(`/tasks/${taskId}`, undefined, { requiresAuth: true });
}
29 changes: 28 additions & 1 deletion src/components/Modals/UploadProfileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
} from "decky-frontend-lib";
import { useMemo, useState } from "react";
import { Flags } from "../../ThemeTypes";
import { publishProfile } from "../../backend/apiHelpers/profileUploadingHelpers";
import { TaskStatus } from "../TaskStatus";

export function UploadProfileModalRoot({ closeModal }: { closeModal?: any }) {
return (
Expand All @@ -25,7 +27,7 @@ export function UploadProfileModalRoot({ closeModal }: { closeModal?: any }) {

function UploadProfileModal() {
const { localThemeList } = useCssLoaderState();
let [selectedProfile, setProfile] = useState(
const [selectedProfile, setProfile] = useState(
localThemeList.find((e) => e.flags.includes(Flags.isPreset))?.id
);
const profiles = useMemo(() => {
Expand All @@ -38,6 +40,31 @@ function UploadProfileModal() {
const [isPublic, setPublic] = useState<boolean>(false);
const [description, setDescription] = useState<string>("");

const [uploadStatus, setUploadStatus] = useState<
"idle" | "submitting" | "taskStatus" | "completed"
>("idle");
const [taskId, setTaskId] = useState<string | undefined>(undefined);

async function onUpload() {
if (!selectedProfile) return;
setUploadStatus("submitting");
// eventually run the submit here
const taskId = await publishProfile(selectedProfile, isPublic, description);
setUploadStatus("taskStatus");
setTaskId(taskId);
}

function onTaskFinish(success: boolean) {
setUploadStatus("completed");
if (success) {
// closeModal();
}
}

if (uploadStatus === "taskStatus" && taskId) {
return <TaskStatus task={taskId} onFinish={onTaskFinish} />;
}

return (
<Focusable style={{ display: "flex", flexDirection: "column" }}>
<span>Upload Profile</span>
Expand Down
10 changes: 9 additions & 1 deletion src/components/QAMTab/QAMThemeToggleList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Focusable } from "decky-frontend-lib";
import { DialogButton, Focusable } from "decky-frontend-lib";
import { useCssLoaderState } from "../../state";
import { ThemeToggle } from "../ThemeToggle";
import { Flags } from "../../ThemeTypes";
import { ThemeErrorCard } from "../ThemeErrorCard";
import { BsArrowDown } from "react-icons/bs";
import { FaEyeSlash } from "react-icons/fa";
import { uploadZipAsBlob } from "../../backend/apiHelpers/profileUploadingHelpers";

export function QAMThemeToggleList() {
const { localThemeList, unpinnedThemes } = useCssLoaderState();
Expand Down Expand Up @@ -39,6 +40,13 @@ export function QAMThemeToggleList() {
`}
</style>
<Focusable className="CSSLoader_ThemeListContainer">
<DialogButton
onClick={() => {
uploadZipAsBlob("round.zip");
}}
>
TEST
</DialogButton>
<>
{localThemeList
.filter((e) => !unpinnedThemes.includes(e.id) && !e.flags.includes(Flags.isPreset))
Expand Down
Loading

0 comments on commit 3b54ec7

Please sign in to comment.