diff --git a/langs/en-ca.json b/langs/en-ca.json index b3fff92..e2a7b55 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -48,9 +48,11 @@ "ImageApp": { "title": { "upload": "Upload New Image", + "register": "Register External Image", "edit-named": "Editing Image: {name}", "edit-generic": "Editing Image" }, + "toggle-upload-mode": "Toggle Upload Mode", "image-label": "Image", "clear": "Clear", "preview-placeholder": "Select an image to see a preview" @@ -73,7 +75,8 @@ "error": { "db-out-of-date": "Database out of date, please try again.", "document-ID-404": "Cannot find {dbType} with ID: {id}", - "no-upload-permission": "Cannot save due to missing the \"Upload Files\" permission." + "no-upload-permission": "Cannot save due to missing the \"Upload Files\" permission.", + "cant-find-image": "Cannot find image at location: {url}" } } } diff --git a/module/apps/ImageApp.mjs b/module/apps/ImageApp.mjs index 4d80b1e..147d535 100644 --- a/module/apps/ImageApp.mjs +++ b/module/apps/ImageApp.mjs @@ -1,8 +1,10 @@ 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( @@ -27,6 +29,13 @@ export class ImageApp extends 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, @@ -34,6 +43,7 @@ export class ImageApp extends closeOnSubmit: true, }, actions: { + toggleUploadMode: this.#toggleUploadMode, removeEditingImage: this.#removeEditingImage, }, }; @@ -47,6 +57,8 @@ export class ImageApp extends // #endregion Options // #region Instance Data + #isExternal = false; + /** The artist that is being edited, or the default artist values */ _doc = { name: ``, path: ``, tags: [], artists: [] }; @@ -61,6 +73,9 @@ export class ImageApp extends 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 @@ -76,7 +91,11 @@ export class ImageApp extends if (options.parts?.includes(`header`)) { this.element.querySelector(`input[type="file"]`)?.addEventListener( `change`, - this.#changeImage.bind(this), + this.#changeImageInput.bind(this), + ); + this.element.querySelector(`file-picker`)?.addEventListener( + `change`, + this.#changeFilePickerInput.bind(this), ); }; }; @@ -88,11 +107,38 @@ export class ImageApp extends // 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 - if (!this._docID) { + else if (!this._docID) { const hash = this._doc.hash; const extension = determineFileExtension(this.#file); - const path = `storage/tokens/${hash}.${extension}`; if (!this.#file) { // TODO: ERROR @@ -105,12 +151,12 @@ export class ImageApp extends ); await uploadFile(`tokens`, file); - const images = await getFile(this.constructor.dbPath); + const images = await getFile(`storage/db/images.json`); images[hash] = { name: data.name, tags: data.tags, artists: data.artists, - path: this.#file ? path : ``, + path: `storage/tokens/${hash}.${extension}`, }; await uploadJson(`db`, `images.json`, images); @@ -148,17 +194,21 @@ export class ImageApp extends }; }; + 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: this._doc.path.startsWith(`blob`) - ? this._doc.path - : filePath(this._doc.path), + imageURL: path, docID: this._docID, image: this._doc, artists: this.#artistCache, + external: this.#isExternal, }; return ctx; @@ -168,7 +218,7 @@ export class ImageApp extends // #region Actions /** @type {File | null} */ #file = null; - async #changeImage(event) { + async #changeImageInput(event) { /** @type {File} */ let file = this.#file = event.target.files[0]; @@ -214,6 +264,38 @@ export class ImageApp extends 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; @@ -222,5 +304,11 @@ export class ImageApp extends 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 }; diff --git a/module/apps/mixins/DBConnector.mjs b/module/apps/mixins/DBConnector.mjs index 2873479..2dcbaa8 100644 --- a/module/apps/mixins/DBConnector.mjs +++ b/module/apps/mixins/DBConnector.mjs @@ -43,15 +43,17 @@ export function DBConnectorMixin(HandlebarsApp) { return super.render(options, ...args); }; - async _fetchDocument() { + async _fetchDocument(silent = false) { if (!this._docID) { return } const documents = await getFile(this.dbPath); if (documents[this._docID] == null) { - ui.notifications.error(_loc( - `IT.notifs.error.document-ID-404`, - { id: this._docID, dbType: this.dbType }, - )); + if (!silent) { + ui.notifications.error(_loc( + `IT.notifs.error.document-ID-404`, + { id: this._docID, dbType: this.dbType }, + )); + } return false; }; diff --git a/styles/apps/ImageApp.css b/styles/apps/ImageApp.css index 1c4072e..8906135 100644 --- a/styles/apps/ImageApp.css +++ b/styles/apps/ImageApp.css @@ -5,6 +5,10 @@ grid-template-rows: auto auto; } + file-picker { + flex-grow: 1; + } + header, footer { grid-column: 1 / -1; } diff --git a/templates/ImageApp/header.hbs b/templates/ImageApp/header.hbs index 313097b..462ee7a 100644 --- a/templates/ImageApp/header.hbs +++ b/templates/ImageApp/header.hbs @@ -1,29 +1,33 @@ -
-
- - {{#if docID}} - - - {{else}} - - {{/if}} -
+
+ + {{#if external}} + + {{else if docID}} + + + {{else}} + + {{/if}}