import TokenCustomConfig from './tokenCustomConfig.js'; import { isVideo, isImage, keyPressed, SEARCH_TYPE, BASE_IMAGE_CATEGORIES, getFileName } from '../scripts/utils.js'; import { showArtSelect } from '../token-variants.mjs'; import { TVA_CONFIG, getSearchOptions } from '../scripts/settings.js'; export function addToArtSelectQueue(search, options) { ArtSelect.queue.push({ search: search, options: options, }); $('button#token-variant-art-clear-queue').html(`Clear Queue (${ArtSelect.queue.length})`).show(); } export function addToQueue(search, options) { ArtSelect.queue.push({ search: search, options: options, }); } export function renderFromQueue(force = false) { if (!force) { const artSelects = Object.values(ui.windows).filter((app) => app instanceof ArtSelect); if (artSelects.length !== 0) { if (ArtSelect.queue.length !== 0) $('button#token-variant-art-clear-queue').html(`Clear Queue (${ArtSelect.queue.length})`).show(); return; } } let callData = ArtSelect.queue.shift(); if (callData?.options.execute) { callData.options.execute(); callData = ArtSelect.queue.shift(); } if (callData) { showArtSelect(callData.search, callData.options); } } function delay(fn, ms) { let timer = 0; return function (...args) { clearTimeout(timer); timer = setTimeout(fn.bind(this, ...args), ms || 0); }; } export class ArtSelect extends FormApplication { static queue = []; static instance = null; // showArtSelect(...) can take a while to fully execute and it is possible for it to be called // multiple times in very quick succession especially if copy pasting tokens or importing actors. // This variable set early in the function execution is used to queue additional requests rather // than continue execution static executing = false; static IMAGE_DISPLAY = { NONE: 0, PORTRAIT: 1, TOKEN: 2, PORTRAIT_TOKEN: 3, IMAGE: 4, }; constructor( search, { preventClose = false, object = null, callback = null, searchType = null, allImages = null, image1 = '', image2 = '', displayMode = ArtSelect.IMAGE_DISPLAY.NONE, multipleSelection = false, searchOptions = {}, } = {} ) { let title = game.i18n.localize('token-variants.windows.art-select.select-variant'); if (searchType === SEARCH_TYPE.TOKEN) title = game.i18n.localize('token-variants.windows.art-select.select-token-art'); else if (searchType === SEARCH_TYPE.PORTRAIT) title = game.i18n.localize('token-variants.windows.art-select.select-portrait-art'); super( {}, { closeOnSubmit: false, width: ArtSelect.WIDTH || 500, height: ArtSelect.HEIGHT || 500, left: ArtSelect.LEFT, top: ArtSelect.TOP, title: title, } ); this.search = search; this.allImages = allImages; this.callback = callback; this.doc = object; this.preventClose = preventClose; this.image1 = image1; this.image2 = image2; this.displayMode = displayMode; this.multipleSelection = multipleSelection; this.searchType = searchType; this.searchOptions = mergeObject(searchOptions, getSearchOptions(), { overwrite: false, }); ArtSelect.instance = this; const constructorName = `ArtSelect`; Object.defineProperty(ArtSelect.prototype.constructor, 'name', { value: constructorName }); } static get defaultOptions() { return mergeObject(super.defaultOptions, { id: 'token-variants-art-select', classes: ['sheet'], template: 'modules/token-variants/templates/artSelect.html', resizable: true, minimizable: false, }); } _getHeaderButtons() { const buttons = super._getHeaderButtons(); buttons.unshift({ label: 'FilePicker', class: 'file-picker', icon: 'fas fa-file-import fa-fw', onclick: () => { new FilePicker({ type: 'imagevideo', callback: (path) => { if (!this.preventClose) { this.close(); } if (this.callback) { this.callback(path, getFileName(path)); } }, }).render(); }, }); buttons.unshift({ label: 'Image Category', class: 'type', icon: 'fas fa-swatchbook', onclick: () => { if (ArtSelect.instance) ArtSelect.instance._typeSelect(); }, }); return buttons; } _typeSelect() { const categories = BASE_IMAGE_CATEGORIES.concat(TVA_CONFIG.customImageCategories); const buttons = {}; for (const c of categories) { let label = c; if (c === this.searchType) { label = '>>> ' + label + ' <<<'; } buttons[c] = { label: label, callback: () => { if (this.searchType !== c) { this.searchType = c; this._performSearch(this.search, true); } }, }; } new Dialog({ title: `Select Image Category and Filter`, content: ``, buttons: buttons, }).render(true); } async getData(options) { const data = super.getData(options); if (this.doc instanceof Item) { data.item = true; data.description = this.doc.system?.description?.value ?? ''; } const searchOptions = this.searchOptions; const algorithm = searchOptions.algorithm; // // Create buttons // const tokenConfigs = (TVA_CONFIG.tokenConfigs || []).flat(); const fuzzySearch = algorithm.fuzzy; let allButtons = new Map(); let artFound = false; const genLabel = function (str, indices, start = '', end = '', fillChar = null) { if (!indices) return str; let fillStr = fillChar ? fillChar.repeat(str.length) : str; let label = ''; let lastIndex = 0; for (const index of indices) { label += fillStr.slice(lastIndex, index[0]); label += start + str.slice(index[0], index[1] + 1) + end; lastIndex = index[1] + 1; } label += fillStr.slice(lastIndex, fillStr.length); return label; }; const genTitle = function (obj) { if (!fuzzySearch) return obj.path; let percent = Math.ceil((1 - obj.score) * 100) + '%'; if (searchOptions.runSearchOnPath) { return percent + '\n' + genLabel(obj.path, obj.indices, '', '', '-') + '\n' + obj.path; } return percent; }; this.allImages.forEach((images, search) => { const buttons = []; images.forEach((imageObj) => { artFound = true; const vid = isVideo(imageObj.path); const img = isImage(imageObj.path); buttons.push({ path: imageObj.path, img: img, vid: vid, type: vid || img, name: imageObj.name, label: fuzzySearch && !searchOptions.runSearchOnPath ? genLabel(imageObj.name, imageObj.indices) : imageObj.name, title: genTitle(imageObj), hasConfig: this.searchType === SEARCH_TYPE.TOKEN || this.searchType === SEARCH_TYPE.PORTRAIT_AND_TOKEN ? Boolean( tokenConfigs.find((config) => config.tvImgSrc == imageObj.path && config.tvImgName == imageObj.name) ) : false, }); }); allButtons.set(search, buttons); }); if (artFound) data.allImages = allButtons; data.search = this.search; data.queue = ArtSelect.queue.length; data.image1 = this.image1; data.image2 = this.image2; data.displayMode = this.displayMode; data.multipleSelection = this.multipleSelection; data.displaySlider = algorithm.fuzzy && algorithm.fuzzyArtSelectPercentSlider; data.fuzzyThreshold = algorithm.fuzzyThreshold; if (data.displaySlider) { data.fuzzyThreshold = 100 - data.fuzzyThreshold * 100; data.fuzzyThreshold = data.fuzzyThreshold.toFixed(0); } data.autoplay = !TVA_CONFIG.playVideoOnHover; return data; } /** * @param {JQuery} html */ activateListeners(html) { super.activateListeners(html); const callback = this.callback; const close = () => this.close(); const object = this.doc; const preventClose = this.preventClose; const multipleSelection = this.multipleSelection; const boxes = html.find(`.token-variants-grid-box`); boxes.hover( function () { if (TVA_CONFIG.playVideoOnHover) { const vid = $(this).siblings('video'); if (vid.length) { vid[0].play(); $(this).siblings('.fa-play').hide(); } } }, function () { if (TVA_CONFIG.pauseVideoOnHoverOut) { const vid = $(this).siblings('video'); if (vid.length) { vid[0].pause(); vid[0].currentTime = 0; $(this).siblings('.fa-play').show(); } } } ); boxes.map((box) => { boxes[box].addEventListener('click', async function (event) { if (keyPressed('config')) { if (object) new TokenCustomConfig(object, {}, event.target.dataset.name, event.target.dataset.filename).render(true); } else { if (!preventClose) { close(); } if (callback) { callback(event.target.dataset.name, event.target.dataset.filename); } } }); if (multipleSelection) { boxes[box].addEventListener('contextmenu', async function (event) { $(event.target).toggleClass('selected'); }); } }); let searchInput = html.find('#custom-art-search'); searchInput.focus(); searchInput[0].selectionStart = searchInput[0].selectionEnd = 10000; searchInput.on( 'input', delay((event) => { this._performSearch(event.target.value); }, 350) ); html.find(`button#token-variant-art-clear-queue`).on('click', (event) => { ArtSelect.queue = ArtSelect.queue.filter((callData) => callData.options.execute); $(event.target).hide(); }); $(html) .find('[name="fuzzyThreshold"]') .change((e) => { $(e.target) .siblings('.token-variants-range-value') .html(`${parseFloat(e.target.value).toFixed(0)}%`); this.searchOptions.algorithm.fuzzyThreshold = (100 - e.target.value) / 100; }) .change( delay((event) => { this._performSearch(this.search, true); }, 350) ); if (multipleSelection) { html.find(`button#token-variant-art-return-selected`).on('click', () => { if (callback) { const images = []; html .find(`.token-variants-grid-box.selected`) .siblings('.token-variants-grid-image') .each(function () { images.push(this.getAttribute('src')); }); callback(images); } close(); }); html.find(`button#token-variant-art-return-all`).on('click', () => { if (callback) { const images = []; html.find(`.token-variants-grid-image`).each(function () { images.push(this.getAttribute('src')); }); callback(images); } close(); }); } } _performSearch(search, force = false) { if (!force && this.search.trim() === search.trim()) return; showArtSelect(search, { callback: this.callback, searchType: this.searchType, object: this.doc, force: true, image1: this.image1, image2: this.image2, displayMode: this.displayMode, multipleSelection: this.multipleSelection, searchOptions: this.searchOptions, preventClose: this.preventClose, }); } /** * @param {Event} event * @param {Object} formData */ async _updateObject(event, formData) { if (formData && formData.search != this.search) { this._performSearch(formData.search); } else { this.close(); } } setPosition(options = {}) { if (options.width) ArtSelect.WIDTH = options.width; if (options.height) ArtSelect.HEIGHT = options.height; if (options.top) ArtSelect.TOP = options.top; if (options.left) ArtSelect.LEFT = options.left; super.setPosition(options); } async close(options = {}) { let callData = ArtSelect.queue.shift(); if (callData?.options.execute) { callData.options.execute(); callData = ArtSelect.queue.shift(); } if (callData) { callData.options.force = true; showArtSelect(callData.search, callData.options); } else { // For some reason there might be app instances that have not closed themselves by this point // If there are, close them now const artSelects = Object.values(ui.windows) .filter((app) => app instanceof ArtSelect) .filter((app) => app.appId !== this.appId); for (const app of artSelects) { app.close(); } return super.close(options); } } } export function insertArtSelectButton(html, target, { search = '', searchType = SEARCH_TYPE.TOKEN } = {}) { const button = $(``); button.on('click', () => { showArtSelect(search, { callback: (imgSrc, name) => { button.siblings(`[name="${target}"]`).val(imgSrc); }, searchType, }); }); const input = html.find(`[name="${target}"]`); input.after(button); return Boolean(input.length); }