All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

465 lines
14 KiB

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