|
|
- import {
- getFileName,
- isImage,
- isVideo,
- SEARCH_TYPE,
- keyPressed,
- updateActorImage,
- updateTokenImage,
- decodeURISafely,
- } from '../scripts/utils.js';
- import TokenCustomConfig from './tokenCustomConfig.js';
- import EffectMappingForm from './effectMappingForm.js';
- import { TVA_CONFIG } from '../scripts/settings.js';
- import UserList from './userList.js';
- import FlagsConfig from './flagsConfig.js';
- import RandomizerConfig from './randomizerConfig.js';
- import { doImageSearch, findImagesFuzzy } from '../scripts/search.js';
-
- export const TOKEN_HUD_VARIANTS = { variants: null, actor: null };
-
- export async function renderTokenHUD(hud, html, token, searchText = '', fp_files = null) {
- activateStatusEffectListeners(token);
-
- const hudSettings = TVA_CONFIG.hud;
- const FULL_ACCESS = TVA_CONFIG.permissions.hudFullAccess[game.user.role];
- const PARTIAL_ACCESS = TVA_CONFIG.permissions.hud[game.user.role];
-
- // Check if the HUD button should be displayed
- if (
- !hudSettings.enableSideMenu ||
- (!PARTIAL_ACCESS && !FULL_ACCESS) ||
- token.flags['token-variants']?.disableHUDButton
- )
- return;
-
- const tokenActor = game.actors.get(token.actorId);
-
- // Disable button if Token HUD Wildcard is enabled and appropriate setting is set
- if (TVA_CONFIG.worldHud.disableIfTHWEnabled && game.modules.get('token-hud-wildcard')?.active) {
- if (tokenActor && tokenActor.prototypeToken.randomImg) return;
- }
-
- const button = $(`
- <div class="control-icon" data-action="token-variants-side-selector">
- <img
- id="token-variants-side-button"
- src="modules/token-variants/img/token-images.svg"
- width="36"
- height="36"
- title="Left-click: Image Menu
Right-click: Search & Additional settings"
- />
- </div>
- `);
-
- html.find('div.right').last().append(button);
- html.find('div.right').click(_deactivateTokenVariantsSideSelector);
-
- button.click((event) => _onButtonClick(event, token));
- if (FULL_ACCESS) {
- button.contextmenu((event) => _onButtonRightClick(event, hud, html, token));
- }
- }
-
- async function _onButtonClick(event, token) {
- const button = $(event.target).closest('.control-icon');
-
- // De-activate 'Status Effects'
- button.closest('div.right').find('div.control-icon.effects').removeClass('active');
- button.closest('div.right').find('.status-effects').removeClass('active');
-
- // Remove contextmenu
- button.find('.contextmenu').remove();
-
- // Toggle variants side menu
- button.toggleClass('active');
- let variantsWrap = button.find('.token-variants-wrap');
- if (button.hasClass('active')) {
- if (!variantsWrap.length) {
- variantsWrap = await renderSideSelect(token);
- if (variantsWrap) button.find('img').after(variantsWrap);
- else return;
- }
- variantsWrap.addClass('active');
- } else {
- variantsWrap.removeClass('active');
- }
- }
-
- function _onButtonRightClick(event, hud, html, token) {
- // Display side menu if button is not active yet
- const button = $(event.target).closest('.control-icon');
- if (!button.hasClass('active')) {
- // button.trigger('click');
- button.addClass('active');
- }
-
- if (button.find('.contextmenu').length) {
- // Contextmenu already displayed. Remove and activate images
- button.find('.contextmenu').remove();
- button.removeClass('active').trigger('click');
- //button.find('.token-variants-wrap.images').addClass('active');
- } else {
- // Contextmenu is not displayed. Hide images, create it and add it
- button.find('.token-variants-wrap.images').removeClass('active');
- const contextMenu = $(`
- <div class="token-variants-wrap contextmenu active">
- <div class="token-variants-context-menu active">
- <input class="token-variants-side-search" type="text" />
- <button class="flags" type="button"><i class="fab fa-font-awesome-flag"></i><label>Flags</label></button>
- <button class="file-picker" type="button"><i class="fas fa-file-import fa-fw"></i><label>Browse Folders</label></button>
- <button class="effectConfig" type="button"><i class="fas fa-sun"></i><label>Mappings</label></button>
- <button class="randomizerConfig" type="button"><i class="fas fa-dice"></i><label>Randomizer</label></button>
- </div>
- </div>
- `);
- button.append(contextMenu);
-
- // Register contextmenu listeners
- contextMenu
- .find('.token-variants-side-search')
- .on('keyup', (event) => _onImageSearchKeyUp(event, token))
- .on('click', (event) => {
- event.preventDefault();
- event.stopPropagation();
- });
- contextMenu.find('.flags').click((event) => {
- const tkn = canvas.tokens.get(token._id);
- if (tkn) {
- event.preventDefault();
- event.stopPropagation();
- new FlagsConfig(tkn).render(true);
- }
- });
- contextMenu.find('.file-picker').click(async (event) => {
- event.preventDefault();
- event.stopPropagation();
- new FilePicker({
- type: 'imagevideo',
- callback: async (path, fp) => {
- const content = await FilePicker.browse(fp.activeSource, fp.result.target);
- let files = content.files.filter((f) => isImage(f) || isVideo(f));
- if (files.length) {
- button.find('.token-variants-wrap').remove();
- const sideSelect = await renderSideSelect(token, '', files);
- if (sideSelect) {
- sideSelect.addClass('active');
- button.append(sideSelect);
- }
- }
- },
- }).render(true);
- });
- contextMenu.find('.effectConfig').click((event) => {
- new EffectMappingForm(token).render(true);
- });
- contextMenu.find('.randomizerConfig').click((event) => {
- new RandomizerConfig(token).render(true);
- });
- }
- }
-
- function _deactivateTokenVariantsSideSelector(event) {
- const controlIcon = $(event.target).closest('.control-icon');
- const dataAction = controlIcon.attr('data-action');
-
- switch (dataAction) {
- case 'effects':
- break; // Effects button
- case 'thwildcard-selector':
- break; // Token HUD Wildcard module button
- default:
- return;
- }
-
- $(event.target)
- .closest('div.right')
- .find('.control-icon[data-action="token-variants-side-selector"]')
- .removeClass('active');
- $(event.target).closest('div.right').find('.token-variants-wrap').removeClass('active');
- }
-
- async function renderSideSelect(token, searchText = '', fp_files = null) {
- const hudSettings = TVA_CONFIG.hud;
- const worldHudSettings = TVA_CONFIG.worldHud;
- const FULL_ACCESS = TVA_CONFIG.permissions.hudFullAccess[game.user.role];
- const PARTIAL_ACCESS = TVA_CONFIG.permissions.hud[game.user.role];
-
- const tokenActor = game.actors.get(token.actorId);
-
- let images = [];
- let actorVariants = [];
- let imageDuplicates = new Set();
- const pushImage = (img) => {
- if (imageDuplicates.has(img.path)) {
- if (!images.find((obj) => obj.path === img.path && obj.name === img.name)) {
- images.push(img);
- }
- } else {
- images.push(img);
- imageDuplicates.add(img.path);
- }
- };
-
- actorVariants = getVariants(tokenActor);
-
- if (!fp_files) {
- if (!searchText) {
- // Insert current token image
- if (token.texture?.src && token.texture?.src !== CONST.DEFAULT_TOKEN) {
- pushImage({
- path: decodeURISafely(token.texture.src),
- name: token.flags?.['token-variants']?.name ?? getFileName(token.texture.src),
- });
- }
-
- if (tokenActor) {
- // Insert default token image
- const defaultImg =
- tokenActor.prototypeToken?.flags['token-variants']?.['randomImgDefault'] ||
- tokenActor.prototypeToken?.flags['token-hud-wildcard']?.['default'] ||
- '';
- if (defaultImg) {
- pushImage({ path: decodeURISafely(defaultImg), name: getFileName(defaultImg) });
- }
-
- if (FULL_ACCESS || PARTIAL_ACCESS) {
- actorVariants.forEach((variant) => {
- for (const name of variant.names) {
- pushImage({ path: decodeURISafely(variant.imgSrc), name: name });
- }
- });
- }
-
- // Parse directory flag and include the images
- if (FULL_ACCESS || PARTIAL_ACCESS) {
- const directoryFlag = tokenActor.getFlag('token-variants', 'directory');
- if (directoryFlag) {
- let dirFlagImages;
- try {
- let path = directoryFlag.path;
- let source = directoryFlag.source;
- let bucket = '';
- if (source.startsWith('s3:')) {
- bucket = source.substring(3, source.length);
- source = 's3';
- }
- const content = await FilePicker.browse(source, path, {
- type: 'imagevideo',
- bucket,
- });
- dirFlagImages = content.files;
- } catch (err) {
- dirFlagImages = [];
- }
- dirFlagImages = dirFlagImages.forEach((f) => {
- if (isImage(f) || isVideo(f)) pushImage({ path: decodeURISafely(f), name: getFileName(f) });
- });
- }
- }
-
- if (
- (FULL_ACCESS || PARTIAL_ACCESS) &&
- worldHudSettings.includeWildcard &&
- !worldHudSettings.displayOnlySharedImages
- ) {
- // Merge wildcard images
- const protoImg = tokenActor.prototypeToken.texture.src;
- if (tokenActor.prototypeToken.randomImg) {
- (await tokenActor.getTokenImages())
- .filter((img) => !img.includes('*'))
- .forEach((img) => {
- pushImage({ path: decodeURISafely(img), name: getFileName(img) });
- });
- } else if (protoImg.includes('*') || protoImg.includes('{') || protoImg.includes('}')) {
- // Modified version of Actor.getTokenImages()
- const getTokenImages = async () => {
- if (tokenActor._tokenImages) return tokenActor._tokenImages;
-
- let source = 'data';
- let pattern = tokenActor.prototypeToken.texture.src;
- const browseOptions = { wildcard: true };
-
- // Support non-user sources
- if (/\.s3\./.test(pattern)) {
- source = 's3';
- const { bucket, keyPrefix } = FilePicker.parseS3URL(pattern);
- if (bucket) {
- browseOptions.bucket = bucket;
- pattern = keyPrefix;
- }
- } else if (pattern.startsWith('icons/')) source = 'public';
-
- // Retrieve wildcard content
- try {
- const content = await FilePicker.browse(source, pattern, browseOptions);
- tokenActor._tokenImages = content.files;
- } catch (err) {
- tokenActor._tokenImages = [];
- }
- return tokenActor._tokenImages;
- };
-
- (await getTokenImages())
- .filter((img) => !img.includes('*') && (isImage(img) || isVideo(img)))
- .forEach((variant) => {
- pushImage({ path: decodeURISafely(variant), name: getFileName(variant) });
- });
- }
- }
- }
- }
-
- // Perform image search if needed
- if (FULL_ACCESS) {
- let search;
- if (searchText) {
- search = searchText.length > 2 ? searchText : null;
- } else {
- if (worldHudSettings.displayOnlySharedImages || tokenActor?.getFlag('token-variants', 'disableNameSearch')) {
- // No search
- } else if (token.name.length > 2) {
- search = token.name;
- }
- }
-
- if (search) {
- let artSearch = await doImageSearch(search, {
- searchType: SEARCH_TYPE.TOKEN,
- searchOptions: { keywordSearch: worldHudSettings.includeKeywords },
- });
-
- // Merge full search, and keywords into a single array
- if (artSearch) {
- artSearch.forEach((results) => {
- results.forEach((img) => pushImage(img));
- });
- }
- }
- }
- } else {
- images = fp_files.map((f) => {
- return { path: decodeURISafely(f), name: getFileName(f) };
- });
- }
-
- // Retrieving the possibly custom name attached as a flag to the token
- let tokenImageName = '';
- if (token.flags['token-variants'] && token.flags['token-variants']['name']) {
- tokenImageName = token.flags['token-variants']['name'];
- } else {
- tokenImageName = getFileName(token.texture.src);
- }
-
- let imagesParsed = [];
- const tokenConfigs = (TVA_CONFIG.tokenConfigs || []).flat();
- const tkn = canvas.tokens.get(token._id);
- const userMappings = tkn.document.getFlag('token-variants', 'userMappings') || {};
-
- for (const imageObj of images) {
- const img = isImage(imageObj.path);
- const vid = isVideo(imageObj.path);
-
- const hasConfig = Boolean(
- tokenConfigs.find((config) => config.tvImgSrc === imageObj.path && config.tvImgName === imageObj.name)
- );
- let shared = false;
- if (TVA_CONFIG.permissions.hudFullAccess[game.user.role]) {
- actorVariants.forEach((variant) => {
- if (variant.imgSrc === imageObj.path && variant.names.includes(imageObj.name)) {
- shared = true;
- }
- });
- }
-
- const [title, style] = genTitleAndStyle(userMappings, imageObj.path, imageObj.name);
-
- imagesParsed.push({
- route: imageObj.path,
- name: imageObj.name,
- used: imageObj.path === token.texture.src && imageObj.name === tokenImageName,
- img,
- vid,
- unknownType: !img && !vid,
- shared: shared,
- hasConfig: hasConfig,
- title: title,
- style: game.user.isGM && style ? 'box-shadow: ' + style + ';' : null,
- });
- }
-
- //
- // Render
- //
- const imageDisplay = hudSettings.displayAsImage;
- const imageOpacity = hudSettings.imageOpacity / 100;
-
- const sideSelect = $(
- await renderTemplate('modules/token-variants/templates/sideSelect.html', {
- imagesParsed,
- imageDisplay,
- imageOpacity,
- tokenHud: true,
- })
- );
-
- // Activate listeners
- sideSelect.find('video').hover(
- function () {
- if (TVA_CONFIG.playVideoOnHover) {
- this.play();
- $(this).siblings('.fa-play').hide();
- }
- },
- function () {
- if (TVA_CONFIG.pauseVideoOnHoverOut) {
- this.pause();
- this.currentTime = 0;
- $(this).siblings('.fa-play').show();
- }
- }
- );
- sideSelect.find('.token-variants-button-select').click((event) => _onImageClick(event, token._id));
-
- if (FULL_ACCESS) {
- sideSelect.find('.token-variants-button-select').on('contextmenu', (event) => _onImageRightClick(event, token._id));
- }
-
- return sideSelect;
- }
-
- async function _onImageClick(event, tokenId) {
- event.preventDefault();
- event.stopPropagation();
-
- const token = canvas.tokens.controlled.find((t) => t.document.id === tokenId);
- if (!token) return;
-
- const worldHudSettings = TVA_CONFIG.worldHud;
-
- const imgButton = $(event.target).closest('.token-variants-button-select');
- const imgSrc = imgButton.attr('data-name');
- const name = imgButton.attr('data-filename');
-
- if (!imgSrc || !name) return;
-
- if (keyPressed('config') && game.user.isGM) {
- const toggleCog = (saved) => {
- const cog = imgButton.find('.fa-cog');
- if (saved) {
- cog.addClass('active');
- } else {
- cog.removeClass('active');
- }
- };
- new TokenCustomConfig(token, {}, imgSrc, name, toggleCog).render(true);
- } else if (token.document.texture.src === imgSrc) {
- let tokenImageName = token.document.getFlag('token-variants', 'name');
- if (!tokenImageName) tokenImageName = getFileName(token.document.texture.src);
- if (tokenImageName !== name) {
- await updateTokenImage(imgSrc, {
- token: token,
- imgName: name,
- animate: worldHudSettings.animate,
- });
- if (token.actor && worldHudSettings.updateActorImage) {
- if (worldHudSettings.useNameSimilarity) {
- updateActorWithSimilarName(imgSrc, name, token.actor);
- } else {
- updateActorImage(token.actor, imgSrc, { imgName: name });
- }
- }
- }
- } else {
- await updateTokenImage(imgSrc, {
- token: token,
- imgName: name,
- animate: worldHudSettings.animate,
- });
- if (token.actor && worldHudSettings.updateActorImage) {
- if (worldHudSettings.useNameSimilarity) {
- updateActorWithSimilarName(imgSrc, name, token.actor);
- } else {
- updateActorImage(token.actor, imgSrc, { imgName: name });
- }
- }
- }
- }
-
- async function _onImageRightClick(event, tokenId) {
- event.preventDefault();
- event.stopPropagation();
- let token = canvas.tokens.controlled.find((t) => t.document.id === tokenId);
- if (!token) return;
-
- const imgButton = $(event.target).closest('.token-variants-button-select');
- const imgSrc = imgButton.attr('data-name');
- const name = imgButton.attr('data-filename');
-
- if (!imgSrc || !name) return;
-
- if (keyPressed('config') && game.user.isGM) {
- const regenStyle = (token, img) => {
- const mappings = token.document.getFlag('token-variants', 'userMappings') || {};
- const name = imgButton.attr('data-filename');
- const [title, style] = genTitleAndStyle(mappings, img, name);
- imgButton
- .closest('.token-variants-wrap')
- .find(`.token-variants-button-select[data-name='${img}']`)
- .css('box-shadow', style)
- .prop('title', title);
- };
- new UserList(token, imgSrc, regenStyle).render(true);
- } else if (token.actor) {
- let tokenActor = game.actors.get(token.actor.id);
- let variants = getVariants(tokenActor);
-
- // Remove selected variant if present in the flag, add otherwise
- let del = false;
- let updated = false;
- for (let variant of variants) {
- if (variant.imgSrc === imgSrc) {
- let fNames = variant.names.filter((name) => name !== name);
- if (fNames.length === 0) {
- del = true;
- } else if (fNames.length === variant.names.length) {
- fNames.push(name);
- }
- variant.names = fNames;
- updated = true;
- break;
- }
- }
- if (del) variants = variants.filter((variant) => variant.imgSrc !== imgSrc);
- else if (!updated) variants.push({ imgSrc: imgSrc, names: [name] });
-
- // Set shared variants as an actor flag
- setVariants(tokenActor, variants);
- imgButton.find('.fa-share').toggleClass('active'); // Display green arrow
- }
- }
-
- async function _onImageSearchKeyUp(event, token) {
- event.preventDefault();
- event.stopPropagation();
- if (event.key === 'Enter' || event.keyCode === 13) {
- if (event.target.value.length >= 3) {
- const button = $(event.target).closest('.control-icon');
- button.find('.token-variants-wrap').remove();
- const sideSelect = await renderSideSelect(token, event.target.value);
- if (sideSelect) {
- sideSelect.addClass('active');
- button.append(sideSelect);
- }
- }
- }
- }
-
- function genTitleAndStyle(mappings, imgSrc, name) {
- let title = TVA_CONFIG.worldHud.showFullPath ? imgSrc : name;
- let style = '';
- let offset = 2;
- for (const [userId, img] of Object.entries(mappings)) {
- if (img === imgSrc) {
- const user = game.users.get(userId);
- if (!user) continue;
- if (style.length === 0) {
- style = `inset 0 0 0 ${offset}px ${user.color}`;
- } else {
- style += `, inset 0 0 0 ${offset}px ${user.color}`;
- }
- offset += 2;
- title += `\nDisplayed to: ${user.name}`;
- }
- }
- return [title, style];
- }
-
- async function updateActorWithSimilarName(imgSrc, imgName, actor) {
- const results = await findImagesFuzzy(
- imgName,
- SEARCH_TYPE.PORTRAIT,
- {
- algorithm: {
- fuzzyThreshold: 0.4,
- fuzzyLimit: 50,
- },
- },
- true
- );
-
- if (results && results.length !== 0) {
- updateActorImage(actor, results[0].path, { imgName: results[0].name });
- } else {
- updateActorImage(actor, imgSrc, { imgName: imgName });
- }
- }
-
- function activateStatusEffectListeners(token) {
- if (TVA_CONFIG.permissions.statusConfig[game.user.role] && token.actorId && game.actors.get(token.actorId)) {
- $('.control-icon[data-action="effects"]')
- .find('img:first')
- .click((event) => {
- event.preventDefault();
- if (keyPressed('config')) {
- event.stopPropagation();
- new EffectMappingForm(token).render(true);
- }
- });
-
- $('.control-icon[data-action="visibility"]')
- .find('img')
- .click((event) => {
- event.preventDefault();
- if (keyPressed('config')) {
- event.stopPropagation();
- new EffectMappingForm(token, {
- createMapping: { label: 'In Combat', expression: 'token-variants-visibility' },
- }).render(true);
- }
- });
-
- $('.control-icon[data-action="combat"]')
- .find('img')
- .click((event) => {
- event.preventDefault();
- if (keyPressed('config')) {
- event.stopPropagation();
- new EffectMappingForm(token, {
- createMapping: { label: 'In Combat', expression: 'token-variants-combat' },
- }).render(true);
- }
- });
-
- $('.status-effects')
- .find('img')
- .click((event) => {
- event.preventDefault();
- if (keyPressed('config')) {
- event.stopPropagation();
-
- let effectName = event.target.getAttribute('title');
- if (game.system.id === 'pf2e') {
- effectName = $(event.target).closest('picture').attr('title');
- }
- new EffectMappingForm(token, {
- createMapping: { label: effectName, expression: effectName },
- }).render(true);
- }
- });
- }
- }
-
- function getVariants(actor) {
- if (TOKEN_HUD_VARIANTS.variants) return TOKEN_HUD_VARIANTS.variants;
- else return actor?.getFlag('token-variants', 'variants') || [];
- }
-
- function setVariants(actor, variants) {
- TOKEN_HUD_VARIANTS.variants = variants;
- TOKEN_HUD_VARIANTS.actor = actor;
- }
|