312 lines
7.4 KiB
JavaScript
312 lines
7.4 KiB
JavaScript
import { __ID__, filePath } from "../consts.mjs";
|
|
import { getFile, lastModifiedAt } from "../utils/fs.mjs";
|
|
import { imagePath } from "../utils/imagePath.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,
|
|
select: this.#selectImage,
|
|
},
|
|
};
|
|
|
|
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;
|
|
#selectCount = 0;
|
|
#onSubmit = null;
|
|
filters = {
|
|
name: ``,
|
|
tags: [],
|
|
artists: [],
|
|
};
|
|
|
|
constructor({ selectCount = 0, onSubmit = null, ...opts } = {}) {
|
|
super(opts);
|
|
this.#selectCount = selectCount;
|
|
this.#onSubmit = onSubmit;
|
|
};
|
|
|
|
async setPage(page) {
|
|
if (this.#page == page) { return };
|
|
this.#page = page;
|
|
if (this.rendered) {
|
|
return this.render({ parts: [`images`] });
|
|
};
|
|
return;
|
|
};
|
|
|
|
get selectMode() {
|
|
if (this.#selectCount === 0) {
|
|
return `view`;
|
|
} else if (this.#selectCount === 1) {
|
|
return `single`;
|
|
};
|
|
return `multi`;
|
|
};
|
|
// #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),
|
|
);
|
|
};
|
|
};
|
|
|
|
async close(options) {
|
|
this.#submit(null);
|
|
return super.close(options);
|
|
};
|
|
|
|
#submitted = false;
|
|
async #submit(value) {
|
|
if (this.#submitted) return;
|
|
this.#onSubmit?.(value);
|
|
this.#submitted = true;
|
|
this.close();
|
|
};
|
|
|
|
_tearDown(options) {
|
|
super._tearDown(options);
|
|
Hooks.off(`${__ID__}.imageUploaded`, this.#imageUploadHook);
|
|
Hooks.off(`${__ID__}.imageUpdated`, this.#imageUpdateHook);
|
|
};
|
|
// #endregion Lifecycle
|
|
|
|
// #region Data Prep
|
|
_prepareContext() {
|
|
return {
|
|
meta: {
|
|
idp: this.id,
|
|
},
|
|
selectMode: this.selectMode,
|
|
};
|
|
};
|
|
|
|
_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 });
|
|
};
|
|
|
|
#selected = new Set();
|
|
/** @this {ArtBrowser} */
|
|
static async #selectImage(event, target) {
|
|
if (this.#selectCount === 0) { return };
|
|
|
|
const imageID = target.closest(`[data-image-id]`)?.dataset.imageId;
|
|
const image = this.#imagesDB.data[imageID];
|
|
if (!imageID || !image) { return };
|
|
|
|
const path = imagePath(image);
|
|
if (this.#selectCount > 1) {
|
|
if (this.#selected.has(path)) {
|
|
target.ariaPressed = false;
|
|
this.#selected.delete(path);
|
|
} else {
|
|
target.ariaPressed = true;
|
|
this.#selected.add(path);
|
|
};
|
|
if (this.#selected.size >= this.#selectCount) {
|
|
await this.#submit(Array.from(this.#selected));
|
|
};
|
|
} else {
|
|
await this.#submit(path);
|
|
};
|
|
};
|
|
// #endregion Actions
|
|
|
|
// #region Factories
|
|
/**
|
|
* Opens an ArtBrowser intended to get the user to select some number
|
|
* of tokens.
|
|
*
|
|
* @param {number} count The amount of tokens that the user needs to
|
|
* select
|
|
* @returns A promise that resolves to a string, string[], or null
|
|
*/
|
|
static async select(count = 1) {
|
|
if (count < 1) {
|
|
throw "Unable to select 0 or fewer tokens";
|
|
};
|
|
return new Promise((resolve) => {
|
|
const app = new this({
|
|
selectCount: count,
|
|
onSubmit: resolve,
|
|
});
|
|
app.render({ force: true, });
|
|
});
|
|
};
|
|
// #endregion Factories
|
|
};
|