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.
 
 
 

428 lines
14 KiB

import { getFileName, isImage, isVideo, SEARCH_TYPE, keyPressed } from '../scripts/utils.js';
import { TVA_CONFIG } from '../scripts/settings.js';
import FlagsConfig from './flagsConfig.js';
import { doImageSearch } from '../scripts/search.js';
import UserList from './userList.js';
export async function renderTileHUD(hud, html, tileData, searchText = '', fp_files = null) {
const tile = hud.object;
const hudSettings = TVA_CONFIG.hud;
if (!hudSettings.enableSideMenu || !TVA_CONFIG.tilesEnabled) 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="${game.i18n.localize('token-variants.windows.art-select.select-variant')}"
/>
</div>
`);
html.find('div.right').last().append(button);
html.find('div.right').click(_deactivateTokenVariantsSideSelector);
button.click((event) => _onButtonClick(event, tile));
button.contextmenu((event) => _onButtonRightClick(event, tile));
}
async function _onButtonClick(event, tile) {
if (keyPressed('config')) {
setNameDialog(tile);
return;
}
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(tile);
if (variantsWrap) button.find('img').after(variantsWrap);
else return;
}
variantsWrap.addClass('active');
} else {
variantsWrap.removeClass('active');
}
}
function _onButtonRightClick(event, tile) {
// 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>
</div>
</div>
`);
button.append(contextMenu);
// Register contextmenu listeners
contextMenu
.find('.token-variants-side-search')
.on('keydown', (event) => _onImageSearchKeyUp(event, tile))
.on('click', (event) => {
event.preventDefault();
event.stopPropagation();
});
contextMenu.find('.flags').click((event) => {
event.preventDefault();
event.stopPropagation();
new FlagsConfig(tile).render(true);
});
contextMenu.find('.file-picker').click(async (event) => {
event.preventDefault();
event.stopPropagation();
new FilePicker({
type: 'folder',
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(tile, null, files);
if (sideSelect) {
sideSelect.addClass('active');
button.append(sideSelect);
}
}
},
}).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(tile, searchText = null, fp_files = null) {
const hudSettings = TVA_CONFIG.hud;
const worldHudSettings = TVA_CONFIG.worldHud;
const FULL_ACCESS = TVA_CONFIG.permissions.hudFullAccess[game.user.role];
let images = [];
let variants = [];
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);
}
};
if (!fp_files) {
if (searchText !== null && !searchText) return;
if (!searchText) {
variants = tile.document.getFlag('token-variants', 'variants') || [];
variants.forEach((variant) => {
for (const name of variant.names) {
pushImage({ path: variant.imgSrc, name: name });
}
});
// Parse directory flag and include the images
const directoryFlag = tile.document.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.forEach((f) => {
if (isImage(f) || isVideo(f)) pushImage({ path: f, name: getFileName(f) });
});
}
}
// Perform the search if needed
const search = searchText ?? tile.document.getFlag('token-variants', 'tileName');
const noSearch = !search || (!searchText && worldHudSettings.displayOnlySharedImages);
let artSearch = noSearch
? null
: await doImageSearch(search, {
searchType: SEARCH_TYPE.TILE,
searchOptions: { keywordSearch: worldHudSettings.includeKeywords },
});
if (artSearch) {
artSearch.forEach((results) => {
images.push(...results);
});
}
} else {
images = fp_files.map((f) => {
return { path: f, name: getFileName(f) };
});
}
// Retrieving the possibly custom name attached as a flag to the token
let tileImageName = tile.document.getFlag('token-variants', 'name');
if (!tileImageName) {
tileImageName = getFileName(tile.document.texture.src);
}
let imagesParsed = [];
for (const imageObj of images) {
const img = isImage(imageObj.path);
const vid = isVideo(imageObj.path);
let shared = false;
if (game.user.isGM) {
variants.forEach((variant) => {
if (variant.imgSrc === imageObj.path && variant.names.includes(imageObj.name)) {
shared = true;
}
});
}
const userMappings = tile.document.getFlag('token-variants', 'userMappings') || {};
const [title, style] = genTitleAndStyle(userMappings, imageObj.path, imageObj.name);
imagesParsed.push({
route: imageObj.path,
name: imageObj.name,
used: imageObj.path === tile.document.texture.src && imageObj.name === tileImageName,
img,
vid,
unknownType: !img && !vid,
shared: shared,
hasConfig: false, //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,
autoplay: !TVA_CONFIG.playVideoOnHover,
})
);
// 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, tile));
if (FULL_ACCESS) {
sideSelect
.find('.token-variants-button-select')
.on('contextmenu', (event) => _onImageRightClick(event, tile));
}
return sideSelect;
}
async function _onImageClick(event, tile) {
event.preventDefault();
event.stopPropagation();
if (!tile) return;
const imgButton = $(event.target).closest('.token-variants-button-select');
const imgSrc = imgButton.attr('data-name');
const name = imgButton.attr('data-filename');
if (imgSrc) {
canvas.tiles.hud.clear();
await tile.document.update({ img: imgSrc });
try {
if (getFileName(imgSrc) !== name) await tile.document.setFlag('token-variants', 'name', name);
} catch (e) {}
}
}
async function _onImageRightClick(event, tile) {
event.preventDefault();
event.stopPropagation();
if (!tile) 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 = (tile, img) => {
const mappings = tile.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(tile, imgSrc, regenStyle).render(true);
return;
}
let variants = tile.document.getFlag('token-variants', 'variants') || [];
// 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 a flag
tile.document.unsetFlag('token-variants', 'variants');
if (variants.length > 0) {
tile.document.setFlag('token-variants', 'variants', variants);
}
imgButton.find('.fa-share').toggleClass('active'); // Display green arrow
}
async function _onImageSearchKeyUp(event, tile) {
if (event.key === 'Enter' || event.keyCode === 13) {
event.preventDefault();
if (event.target.value.length >= 3) {
const button = $(event.target).closest('.control-icon');
button.find('.token-variants-wrap').remove();
const sideSelect = await renderSideSelect(tile, event.target.value);
if (sideSelect) {
sideSelect.addClass('active');
button.append(sideSelect);
}
}
return false;
}
}
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];
}
function setNameDialog(tile) {
const tileName = tile.document.getFlag('token-variants', 'tileName') || tile.id;
new Dialog({
title: `Assign a name to the Tile (3+ chars)`,
content: `<table style="width:100%"><tr><th style="width:50%"><label>Tile Name</label></th><td style="width:50%"><input type="text" name="input" value="${tileName}"/></td></tr></table>`,
buttons: {
Ok: {
label: `Save`,
callback: (html) => {
const name = html.find('input').val();
if (name) {
canvas.tiles.hud.clear();
tile.document.setFlag('token-variants', 'tileName', name);
}
},
},
},
}).render(true);
}