image-tagger/module/apps/ImageApp.mjs

314 lines
7.9 KiB
JavaScript

import { __ID__, filePath } from "../consts.mjs";
import { convertToWebp, determineFileExtension, getFile, hashFile, lastModifiedAt, uploadFile, uploadJson } from "../utils/fs.mjs";
import { imagePath } from "../utils/imagePath.mjs";
import { DBConnectorMixin } from "./mixins/DBConnector.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
const { fetchWithTimeout } = foundry.utils;
export class ImageApp extends
DBConnectorMixin(
HandlebarsApplicationMixin(
ApplicationV2
)) {
// #region Options
static dbType = `Image`;
static dbPath = `storage/db/images.json`;
static DEFAULT_OPTIONS = {
tag: `form`,
classes: [
__ID__,
`ImageApp`,
],
position: {
width: 600,
},
window: {
contentClasses: [
`standard-form`,
],
controls: [
{
icon: `fa-solid fa-arrow-up-right-from-square`,
label: `IT.apps.ImageApp.toggle-upload-mode`,
action: `toggleUploadMode`,
}
],
},
form: {
handler: this.#onSubmit,
submitOnChange: false,
closeOnSubmit: true,
},
actions: {
toggleUploadMode: this.#toggleUploadMode,
removeEditingImage: this.#removeEditingImage,
},
};
static PARTS = {
header: { template: filePath(`templates/ImageApp/header.hbs`) },
preview: { template: filePath(`templates/ImageApp/preview.hbs`) },
form: { template: filePath(`templates/ImageApp/form.hbs`) },
footer: { template: filePath(`templates/partials/footer.hbs`) },
};
// #endregion Options
// #region Instance Data
#isExternal = false;
/** The artist that is being edited, or the default artist values */
_doc = { name: ``, path: ``, tags: [], artists: [] };
get title() {
if (this._docID) {
if (this._doc.name) {
return _loc(
`IT.apps.ImageApp.title.edit-named`,
{ name: this._doc.name },
);
} else {
return _loc(`IT.apps.ImageApp.title.edit-generic`);
}
};
if (this.#isExternal) {
return _loc(`IT.apps.ImageApp.title.register`);
}
return _loc(`IT.apps.ImageApp.title.upload`);
};
// #endregion Instance Data
// #region Lifecycle
_onFirstRender() {
if (this._doc?.name) {
this._updateFrame({ window: { title: this.title } });
};
};
_onRender(context, options) {
if (options.parts?.includes(`header`)) {
this.element.querySelector(`input[type="file"]`)?.addEventListener(
`change`,
this.#changeImageInput.bind(this),
);
this.element.querySelector(`file-picker`)?.addEventListener(
`change`,
this.#changeFilePickerInput.bind(this),
);
};
};
/** @this {ImageApp} */
static async #onSubmit(event, element, formData) {
const data = formData.object;
// Validate the DB hasn't been updated since opening
if (!(await this.isAbleToSave())) { return };
// Add the external image into the DB
if (this.#isExternal) {
const path = data.path;
const response = await fetchWithTimeout(path);
const type = response.headers.get(`Content-Type`)
if (!Object.values(CONST.IMAGE_FILE_EXTENSIONS).includes(type)) {
// TODO: error
return;
};
const blob = await response.blob();
const hash = await hashFile(blob);
const images = await getFile(`storage/db/images.json`);
images[hash] = {
name: data.name,
tags: data.tags,
artists: data.artists,
external: true,
path,
};
await uploadJson(`db`, `images.json`, images);
Hooks.callAll(`${__ID__}.imageUploaded`, hash, images[hash]);
}
// Upload new image to server
else if (!this._docID) {
const hash = this._doc.hash;
const extension = determineFileExtension(this.#file);
if (!this.#file) {
// TODO: ERROR
return;
}
const file = new File(
[this.#file],
`${hash}.${extension}`,
{ type: this.#file.type },
);
await uploadFile(`tokens`, file);
const images = await getFile(`storage/db/images.json`);
images[hash] = {
name: data.name,
tags: data.tags,
artists: data.artists,
path: `storage/tokens/${hash}.${extension}`,
};
await uploadJson(`db`, `images.json`, images);
Hooks.callAll(`${__ID__}.imageUploaded`, hash, images[hash]);
}
else {
const images = await getFile(this.constructor.dbPath);
Object.assign(images[this._docID], {
name: data.name,
tage: data.tags,
artists: data.artists,
});
await uploadJson(`db`, `images.json`, images);
Hooks.callAll(`${__ID__}.imageUpdated`, this._docID, images[this._docID]);
};
};
// #endregion Lifecycle
// #region Data Prep
#lastArtistEdit = null;
#artistCache = [];
async _prepareContext() {
// Get all of the available artists for the dropdown
const lastModified = await lastModifiedAt(`storage/db/artists.json`);
if (
this.#lastArtistEdit == null
|| lastModified !== this.#lastArtistEdit
) {
this.#artistCache = [];
const artistDB = await getFile(`storage/db/artists.json`);
for (const [id, artist] of Object.entries(artistDB)) {
this.#artistCache.push({ value: id, label: artist.name });
};
};
let path = imagePath(this._doc);
if (this.#isExternal || this._doc.path.startsWith(`blob`)) {
path = this._doc.path;
};
const ctx = {
meta: {
idp: this.id,
},
imageTypes: Object.values(CONST.IMAGE_FILE_EXTENSIONS).join(`,`),
imageURL: path,
docID: this._docID,
image: this._doc,
artists: this.#artistCache,
external: this.#isExternal,
};
return ctx;
};
// #endregion Data Prep
// #region Actions
/** @type {File | null} */
#file = null;
async #changeImageInput(event) {
/** @type {File} */
let file = this.#file = event.target.files[0];
// Prevent memory leaks
URL.revokeObjectURL(this._doc.path);
if (!file) {
this.#file = null;
this._docID = null;
this._doc = { path: ``, name: ``, artists: [], tags: [] };
this._updateFrame({ window: { title: this.title } });
await this.render({ parts: [`header`, `preview`, `form`] });
return;
};
// Ensure we don't already have the file uploaded
const webp = await convertToWebp(file);
let extension;
if (webp) {
file = this.#file = webp;
extension = `webp`;
} else {
extension = determineFileExtension(file);
};
const hash = await hashFile(file);
const path = `storage/tokens/${hash}.${extension}`;
const lastModified = await lastModifiedAt(path);
if (lastModified) {
this._docID = hash;
this._doc.hash = hash;
await this._fetchDocument();
this._updateFrame({ window: { title: this.title } });
await this.render({ parts: [`header`, `preview`, `form`] });
return;
};
// Temporarily blob the image so it can be displayed in-app
const url = URL.createObjectURL(file);
this._doc.name = file.name.replace(`.${extension}`, ``);
this._doc.path = url;
this._doc.hash = hash;
await this.render({ parts: [`preview`, `form`] });
};
async #changeFilePickerInput(event) {
const picker = event.currentTarget;
const path = picker.value;
if (!path) {
this._docID = null;
this._doc = { path, name: ``, artists: [], tags: [] };
this.render({ parts: [ `preview`, `form` ] });
return;
};
let hash;
try {
const response = await fetchWithTimeout(path);
const blob = await response.blob();
hash = await hashFile(blob);
} catch {
ui.notifications.error(_loc(`IT.notifs.error.cant-find-image`, { url: path }));
picker.value = ``;
return;
};
this._docID = hash;
await this._fetchDocument(true);
if (this._doc.path !== path) {
this._docID = null;
this._doc = { path, name: ``, artists: [], tags: [] };
};
this.render({ parts: [ `preview`, `form` ] });
};
/** @this {ImageApp} */
static async #removeEditingImage() {
this.#file = null;
this._docID = null;
this._doc = { path: ``, artists: [], tags: [] };
this._updateFrame({ window: { title: this.title } });
await this.render({ parts: [`header`, `preview`, `form`] });
};
/** @this {ImageApp} */
static async #toggleUploadMode() {
this.#isExternal = !this.#isExternal;
ImageApp.#removeEditingImage.call(this);
};
// #endregion Actions
};