|
|
@ -1,4 +1,4 @@
|
|
|
|
import { Accessor, Component, createSignal, For, Index, onMount, Show } from "solid-js";
|
|
|
|
import { Accessor, Component, createSignal, For, Index, onMount, Setter, Show } from "solid-js";
|
|
|
|
import { GoogleSignIn, googleUser } from "../common/googleAuth";
|
|
|
|
import { GoogleSignIn, googleUser } from "../common/googleAuth";
|
|
|
|
import styles from "./ThumbnailManager.module.scss";
|
|
|
|
import styles from "./ThumbnailManager.module.scss";
|
|
|
|
|
|
|
|
|
|
|
@ -19,6 +19,7 @@ class Template {
|
|
|
|
|
|
|
|
|
|
|
|
const ThumbnailManager: Component = () => {
|
|
|
|
const ThumbnailManager: Component = () => {
|
|
|
|
const [templates, setTemplates] = createSignal<Template[]>([]);
|
|
|
|
const [templates, setTemplates] = createSignal<Template[]>([]);
|
|
|
|
|
|
|
|
const [newTemplateErrors, setNewTemplateErrors] = createSignal<string[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
onMount(async () => {
|
|
|
|
onMount(async () => {
|
|
|
|
const templateDataResponse = await fetch("/thrimshim/templates");
|
|
|
|
const templateDataResponse = await fetch("/thrimshim/templates");
|
|
|
@ -46,6 +47,95 @@ const ThumbnailManager: Component = () => {
|
|
|
|
setTemplates(templateList);
|
|
|
|
setTemplates(templateList);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const submitHandler = async (origName: string, noImageIsError: boolean, errorList: Accessor<string[]>, setErrorList: Setter<string[]>, event: SubmitEvent): Promise<Template | null> => {
|
|
|
|
|
|
|
|
setErrorList([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const form = event.currentTarget as HTMLFormElement;
|
|
|
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const name = formData.get("name") as string;
|
|
|
|
|
|
|
|
const description = formData.get("description") as string;
|
|
|
|
|
|
|
|
const attribution = formData.get("attribution") as string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cropStartX = parseInt(formData.get("cropstartx") as string, 10);
|
|
|
|
|
|
|
|
const cropStartY = parseInt(formData.get("cropstarty") as string, 10);
|
|
|
|
|
|
|
|
const cropEndX = parseInt(formData.get("cropendx") as string, 10);
|
|
|
|
|
|
|
|
const cropEndY = parseInt(formData.get("cropendy") as string, 10);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const locStartX = parseInt(formData.get("locstartx") as string, 10);
|
|
|
|
|
|
|
|
const locStartY = parseInt(formData.get("locstarty") as string, 10);
|
|
|
|
|
|
|
|
const locEndX = parseInt(formData.get("locendx") as string, 10);
|
|
|
|
|
|
|
|
const locEndY = parseInt(formData.get("locendy") as string, 10);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isNaN(cropStartX) || isNaN(cropStartY) || isNaN(cropEndX) || isNaN(cropEndY) || isNaN(locStartX) || isNaN(locStartY) || isNaN(locEndX) || isNaN(locEndY)) {
|
|
|
|
|
|
|
|
setErrorList((errors) => {
|
|
|
|
|
|
|
|
errors.push("All crop and location information must be entered.");
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const imageFile = formData.get("image") as Blob;
|
|
|
|
|
|
|
|
const fileReader = new FileReader();
|
|
|
|
|
|
|
|
const fileReaderCompletePromise = new Promise<void>((resolve, reject) => {
|
|
|
|
|
|
|
|
fileReader.addEventListener("loadend", (event) => resolve());
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
fileReader.readAsDataURL(imageFile);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await fileReaderCompletePromise;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const submitData = new Map();
|
|
|
|
|
|
|
|
submitData.set("name", name);
|
|
|
|
|
|
|
|
submitData.set("description", description);
|
|
|
|
|
|
|
|
submitData.set("attribution", attribution);
|
|
|
|
|
|
|
|
submitData.set("crop", [cropStartX, cropStartY, cropEndX, cropEndY]);
|
|
|
|
|
|
|
|
submitData.set("location", [locStartX, locStartY, locEndX, locEndY]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const imageDataURL = fileReader.result as string;
|
|
|
|
|
|
|
|
if (imageDataURL.startsWith("data:image/png;base64,")) {
|
|
|
|
|
|
|
|
submitData.set("image", imageDataURL.substring(22));
|
|
|
|
|
|
|
|
} else if (noImageIsError) {
|
|
|
|
|
|
|
|
setErrorList((errors) => {
|
|
|
|
|
|
|
|
errors.push("A PNG image must be selected.");
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (googleUser) {
|
|
|
|
|
|
|
|
submitData.set("token", googleUser.getAuthResponse().id_token);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (errorList().length > 0) {
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const submitURL = (origName === "") ? "/thrimshim/add-template" : `/thrimshim/update-template/${encodeURIComponent(origName)}`;
|
|
|
|
|
|
|
|
const submitDataJSON = JSON.stringify(Object.fromEntries(submitData));
|
|
|
|
|
|
|
|
const submitResponse = await fetch(submitURL, {
|
|
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
|
|
body: submitDataJSON,
|
|
|
|
|
|
|
|
headers: { "Content-Type": "application/json" }
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!submitResponse.ok) {
|
|
|
|
|
|
|
|
const errorText = await submitResponse.text();
|
|
|
|
|
|
|
|
setErrorList((errors) => {
|
|
|
|
|
|
|
|
errors.push(errorText);
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
form.reset();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
|
|
description: description,
|
|
|
|
|
|
|
|
attribution: attribution,
|
|
|
|
|
|
|
|
cropStart: { x: cropStartX, y: cropStartY },
|
|
|
|
|
|
|
|
cropEnd: { x: cropEndX, y: cropEndY },
|
|
|
|
|
|
|
|
locationStart: { x: locStartX, y: locStartY },
|
|
|
|
|
|
|
|
locationEnd: { x: locEndX, y: locEndY }
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<>
|
|
|
|
<div class={styles.templatesList}>
|
|
|
|
<div class={styles.templatesList}>
|
|
|
@ -63,96 +153,20 @@ const ThumbnailManager: Component = () => {
|
|
|
|
const [formErrors, setFormErrors] = createSignal<string[]>([]);
|
|
|
|
const [formErrors, setFormErrors] = createSignal<string[]>([]);
|
|
|
|
const [displayImagePreview, setDisplayImagePreview] = createSignal(false);
|
|
|
|
const [displayImagePreview, setDisplayImagePreview] = createSignal(false);
|
|
|
|
const [editing, setEditing] = createSignal(false);
|
|
|
|
const [editing, setEditing] = createSignal(false);
|
|
|
|
let imageEditField;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const formSubmit = async (event: SubmitEvent) => {
|
|
|
|
|
|
|
|
setFormErrors([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const form = event.currentTarget as HTMLFormElement;
|
|
|
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const name = formData.get("name") as string;
|
|
|
|
|
|
|
|
const description = formData.get("description") as string;
|
|
|
|
|
|
|
|
const attribution = formData.get("attribution") as string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cropStartX = parseInt(formData.get("cropstartx") as string, 10);
|
|
|
|
|
|
|
|
const cropStartY = parseInt(formData.get("cropstarty") as string, 10);
|
|
|
|
|
|
|
|
const cropEndX = parseInt(formData.get("cropendx") as string, 10);
|
|
|
|
|
|
|
|
const cropEndY = parseInt(formData.get("cropendy") as string, 10);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const locStartX = parseInt(formData.get("locstartx") as string, 10);
|
|
|
|
|
|
|
|
const locStartY = parseInt(formData.get("locstarty") as string, 10);
|
|
|
|
|
|
|
|
const locEndX = parseInt(formData.get("locendx") as string, 10);
|
|
|
|
|
|
|
|
const locEndY = parseInt(formData.get("locendy") as string, 10);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isNaN(cropStartX) || isNaN(cropStartY) || isNaN(cropEndX) || isNaN(cropEndY) || isNaN(locStartX) || isNaN(locStartY) || isNaN(locEndX) || isNaN(locEndY)) {
|
|
|
|
|
|
|
|
setFormErrors((errors) => {
|
|
|
|
|
|
|
|
errors.push("All crop and location information must be entered.");
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const imageFile = formData.get("image") as Blob;
|
|
|
|
|
|
|
|
const fileReader = new FileReader();
|
|
|
|
|
|
|
|
const fileReaderCompletePromise = new Promise<void>((resolve, reject) => {
|
|
|
|
|
|
|
|
fileReader.addEventListener("loadend", (event) => resolve());
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
fileReader.readAsDataURL(imageFile);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await fileReaderCompletePromise;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const submitData = new Map();
|
|
|
|
|
|
|
|
submitData.set("name", name);
|
|
|
|
|
|
|
|
submitData.set("description", description);
|
|
|
|
|
|
|
|
submitData.set("attribution", attribution);
|
|
|
|
|
|
|
|
submitData.set("crop", [cropStartX, cropStartY, cropEndX, cropEndY]);
|
|
|
|
|
|
|
|
submitData.set("location", [locStartX, locStartY, locEndX, locEndY]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const imageDataURL = fileReader.result as string;
|
|
|
|
|
|
|
|
if (imageDataURL.startsWith("data:image/png;base64,")) {
|
|
|
|
|
|
|
|
submitData.set("image", imageDataURL.substring(22));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (googleUser) {
|
|
|
|
|
|
|
|
submitData.set("token", googleUser.getAuthResponse().id_token);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (formErrors().length > 0) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const origName = template().name;
|
|
|
|
|
|
|
|
const encodedName = encodeURIComponent(origName);
|
|
|
|
|
|
|
|
const submitDataJSON = JSON.stringify(Object.fromEntries(submitData));
|
|
|
|
|
|
|
|
const submitResponse = await fetch(`/thrimshim/update-template/${encodedName}`, {
|
|
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
|
|
body: submitDataJSON,
|
|
|
|
|
|
|
|
headers: { "Content-Type": "application/json" }
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!submitResponse.ok) {
|
|
|
|
|
|
|
|
const errorText = await submitResponse.text();
|
|
|
|
|
|
|
|
setFormErrors((errors) => {
|
|
|
|
|
|
|
|
errors.push(errorText);
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const newTemplate: Template = {
|
|
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
|
|
description: description,
|
|
|
|
|
|
|
|
attribution: attribution,
|
|
|
|
|
|
|
|
cropStart: { x: cropStartX, y: cropStartY },
|
|
|
|
|
|
|
|
cropEnd: { x: cropEndX, y: cropEndY },
|
|
|
|
|
|
|
|
locationStart: { x: locStartX, y: locStartY },
|
|
|
|
|
|
|
|
locationEnd: { x: locEndX, y: locEndY }
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
setTemplates((templateList) => {
|
|
|
|
|
|
|
|
templateList[index] = newTemplate;
|
|
|
|
|
|
|
|
return templateList;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<form class={styles.templatesListRow} onSubmit={formSubmit}>
|
|
|
|
<form
|
|
|
|
|
|
|
|
class={styles.templatesListRow}
|
|
|
|
|
|
|
|
onSubmit={async (event) => {
|
|
|
|
|
|
|
|
const submitData = await submitHandler(template().name, false, formErrors, setFormErrors, event);
|
|
|
|
|
|
|
|
if (submitData) {
|
|
|
|
|
|
|
|
setTemplates((templateList) => {
|
|
|
|
|
|
|
|
templateList[index] = submitData;
|
|
|
|
|
|
|
|
return templateList;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
>
|
|
|
|
<Show
|
|
|
|
<Show
|
|
|
|
when={editing()}
|
|
|
|
when={editing()}
|
|
|
|
fallback={
|
|
|
|
fallback={
|
|
|
@ -237,7 +251,7 @@ const ThumbnailManager: Component = () => {
|
|
|
|
)
|
|
|
|
)
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<input type="file" name="image" accept="image/png" ref={imageEditField} />
|
|
|
|
<input type="file" name="image" accept="image/png" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<button type="submit">Submit</button>
|
|
|
|
<button type="submit">Submit</button>
|
|
|
@ -254,6 +268,120 @@ const ThumbnailManager: Component = () => {
|
|
|
|
}}
|
|
|
|
}}
|
|
|
|
</Index>
|
|
|
|
</Index>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<form
|
|
|
|
|
|
|
|
onSubmit={async (event) => {
|
|
|
|
|
|
|
|
const submitData = await submitHandler("", true, newTemplateErrors, setNewTemplateErrors, event);
|
|
|
|
|
|
|
|
if (submitData) {
|
|
|
|
|
|
|
|
setTemplates((templateList) => [...templateList, submitData]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<h1>Add New Template</h1>
|
|
|
|
|
|
|
|
<ul class={styles.templateUpdateErrors}>
|
|
|
|
|
|
|
|
<For each={newTemplateErrors()}>
|
|
|
|
|
|
|
|
{(error: string, index: Accessor<number>) => <li>{error}</li>}
|
|
|
|
|
|
|
|
</For>
|
|
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
<div class={styles.newTemplateFormFields}>
|
|
|
|
|
|
|
|
<label class={styles.newTemplateFieldLabelContainer}>
|
|
|
|
|
|
|
|
<span>Name:</span>
|
|
|
|
|
|
|
|
<input type="text" name="name" />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class={styles.newTemplateFieldLabelContainer}>
|
|
|
|
|
|
|
|
<span>Image:</span>
|
|
|
|
|
|
|
|
<input type="file" name="image" accept="image/png" />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class={styles.newTemplateFieldLabelContainer}>
|
|
|
|
|
|
|
|
<span>Description:</span>
|
|
|
|
|
|
|
|
<textarea name="description"></textarea>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class={styles.newTemplateFieldLabelContainer}>
|
|
|
|
|
|
|
|
<span>Attribution:</span>
|
|
|
|
|
|
|
|
<input type="text" name="attribution" />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<span>Crop:</span>
|
|
|
|
|
|
|
|
<span>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="cropstartx"
|
|
|
|
|
|
|
|
placeholder="X"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={182}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="cropstarty"
|
|
|
|
|
|
|
|
placeholder="Y"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={0}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<span class={styles.newTemplateMidText}>to</span>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="cropendx"
|
|
|
|
|
|
|
|
placeholder="X"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={1738}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="cropendy"
|
|
|
|
|
|
|
|
placeholder="Y"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={824}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
<span class={styles.newTemplateInlineLabel}>Location:</span>
|
|
|
|
|
|
|
|
<span>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="locstartx"
|
|
|
|
|
|
|
|
placeholder="X"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={45}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="locstarty"
|
|
|
|
|
|
|
|
placeholder="Y"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={45}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<span class={styles.newTemplateMidText}>to</span>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="locendx"
|
|
|
|
|
|
|
|
placeholder="X"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={1235}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
class={styles.templateCoord}
|
|
|
|
|
|
|
|
name="locendy"
|
|
|
|
|
|
|
|
placeholder="Y"
|
|
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
|
|
value={675}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<button type="submit">Add Template</button>
|
|
|
|
|
|
|
|
</form>
|
|
|
|
<GoogleSignIn />
|
|
|
|
<GoogleSignIn />
|
|
|
|
</>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
);
|
|
|
|