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);
}