import { __ID__, filePath } from "../consts.mjs"; import { determineFileExtension, getFile, hashFile, lastModifiedAt, uploadFile, uploadJson } from "../utils/fs.mjs"; import { DBConnectorMixin } from "./mixins/DBConnector.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; 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`, ], }, form: { handler: this.#onSubmit, submitOnChange: false, closeOnSubmit: true, }, actions: { 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 /** The artist that is being edited, or the default artist values */ _doc = { 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`); } }; 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.#changeImage.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 }; // Upload new image to server if (!this._docID) { const hash = this._doc.hash; const extension = determineFileExtension(this.#file); const path = `storage/tokens/${hash}.${extension}`; 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(this.constructor.dbPath); images[hash] = { name: data.name, tags: data.tags, artists: data.artists, path: this.#file ? path : ``, }; 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 }); }; }; const ctx = { meta: { idp: this.id, }, imageTypes: Object.values(CONST.IMAGE_FILE_EXTENSIONS).join(`,`), imageURL: this._doc.path.startsWith(`blob`) ? this._doc.path : filePath(this._doc.path), docID: this._docID, image: this._doc, artists: this.#artistCache, }; return ctx; }; // #endregion Data Prep // #region Actions /** @type {File | null} */ #file = null; async #changeImage(event) { /** @type {File} */ const 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: ``, 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 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.path = url; this._doc.hash = hash; await this.render({ parts: [`preview`] }); }; /** @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`] }); }; // #endregion Actions };