|
|
- 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 = '<b>>>> ' + label + ' <<<</b>';
- }
- 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: `<style>.dialog .dialog-button {flex: 0 0 auto;}</style>`,
- 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 = '<mark>', end = '</mark>', 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
- class="token-variants-image-select-button"
- type="button"
- data-type="imagevideo"
- data-target="${target}"
- title="${game.i18n.localize('token-variants.windows.art-select.select-variant')}">
- <i class="fas fa-images"></i>
- </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);
- }
|