import { __ID__, filePath } from "../consts.mjs"; import { getFile, lastModifiedAt } from "../utils/fs.mjs"; import { paginate } from "../utils/pagination.mjs"; import { ImageApp } from "./ImageApp.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { FormDataExtended } = foundry.applications.ux; const { deepClone } = foundry.utils; const PAGE_SIZE = 52; export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) { // #region Options static DEFAULT_OPTIONS = { classes: [ __ID__, `ArtBrowser`, ], position: {}, window: {}, actions: { nextPage: this.#nextPage, prevPage: this.#prevPage, uploadImage: this.#uploadImage, }, }; static PARTS = { sidebar: { template: filePath(`templates/ArtBrowser/sidebar.hbs`), }, images: { template: filePath(`templates/ArtBrowser/images.hbs`), templates: [ filePath(`templates/ArtBrowser/image/grid.hbs`), ], }, }; // #endregion Options // #region Instance Data #page = 1; filters = { name: ``, tags: [], artists: [], }; async setPage(page) { if (this.#page == page) { return }; this.#page = page; if (this.rendered) { return this.render({ parts: [`images`] }); }; return; }; // #endregion Instance Data // #region Lifecycle #imagesDB = { lastModified: null, data: undefined, }; async #getImages() { const newLastModified = await lastModifiedAt(`storage/db/images.json`); if (this.#imagesDB.lastModified && newLastModified === this.#imagesDB.lastModified) { return; }; this.#imagesDB.lastModified = newLastModified; this.#imagesDB.data = await getFile(`storage/db/images.json`); }; #artistDB = { lastModified: null, data: undefined, }; async #getArtists() { const newLastModified = await lastModifiedAt(`storage/db/artists.json`); if (this.#artistDB.lastModified && newLastModified === this.#artistDB.lastModified) { return; }; this.#artistDB.lastModified = newLastModified; this.#artistDB.data = await getFile(`storage/db/artists.json`); }; async render(...args) { await this.#getArtists(); await this.#getImages(); return super.render(...args); }; #imageUploadHook; #imageUpdateHook; async _onFirstRender(...args) { this.#imageUploadHook = Hooks.on( `${__ID__}.imageUploaded`, () => this.render({ parts: [`images`] }), ); this.#imageUpdateHook = Hooks.on( `${__ID__}.imageUpdated`, () => this.render({ parts: [`images`] }), ); return super._onFirstRender(...args); }; async _onRender(ctx, options) { if (options.parts?.includes(`sidebar`)) { this.element.querySelector(`form.filters`)?.addEventListener( `change`, this.#onFilterSubmit.bind(this), ); }; }; _tearDown(...args) { super._tearDown(...args); Hooks.off(`${__ID__}.imageUploaded`, this.#imageUploadHook); Hooks.off(`${__ID__}.imageUpdated`, this.#imageUpdateHook); }; // #endregion Lifecycle // #region Data Prep _prepareContext() { return { meta: { idp: this.id, }, selectMode: `view`, }; }; _preparePartContext(partID, ctx) { switch (partID) { case `sidebar`: { this._prepareSidebarContext(ctx); break; }; case `images`: { this._prepareImagesContext(ctx); break; }; }; return ctx; }; _prepareSidebarContext(ctx) { // TODO: grab all existing tags for dataset ctx.name = this.filters.name; ctx.tags = this.filters.tags; ctx.artists = this.filters.artists; ctx.artistList = []; for (const [id, artist] of Object.entries(this.#artistDB.data)) { ctx.artistList.push({ value: id, label: artist.name }); }; }; _prepareImagesContext(ctx) { ctx.images = []; const allImages = Object.entries(deepClone(this.#imagesDB.data)); const images = []; for (const [id, image] of allImages) { image.id = id; // Check if it matches the required filters if (this.filters.name && !image.name.includes(this.filters.name)) { continue; }; const hasArtistFilter = this.filters.artists.length > 0; const hasAnArtist = this.filters.artists.some(artist => image.artists.includes(artist)); const hasAllTags = this.filters.tags.every(tag => image.tags.includes(tag)); if ((!hasAnArtist && hasArtistFilter) || !hasAllTags) { continue }; // Convert all of the artist IDs into the actual data image.artists = image.artists .map(artistID => { if (artistID in this.#artistDB.data) { const artist = this.#artistDB.data[artistID]; artist.id = artistID; return artist; }; return { name: artistID, id: artistID }; }); images.push(image); }; // Paginate after filtering and sorting to give page continuity images.sort((a, b) => a.name.localeCompare(b.name)); const paginated = paginate(images, this.#page, PAGE_SIZE); ctx.images = paginated.page; ctx.page = this.#page; ctx.pages = paginated.total; ctx.has = { prev: paginated.prev, next: paginated.next }; }; // #endregion Data Prep // #region Actions async #onFilterSubmit(event) { event.preventDefault(); event.stopPropagation(); const data = (new FormDataExtended(event.currentTarget)).object; this.filters = data; this.render({ parts: [`images`] }); }; /** @this {ArtBrowser} */ static async #nextPage() { this.#page += 1; this.render({ parts: [`images`] }); }; /** @this {ArtBrowser} */ static async #prevPage() { this.#page -= 1; this.render({ parts: [`images`] }); }; /** @this {ArtBrowser} */ static async #uploadImage() { const app = new ImageApp(); app.render({ force: true }); }; // #endregion Actions };