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

  1. import { getFileName, isImage, isVideo, SEARCH_TYPE, keyPressed } from '../scripts/utils.js';
  2. import { TVA_CONFIG } from '../scripts/settings.js';
  3. import FlagsConfig from './flagsConfig.js';
  4. import { doImageSearch } from '../scripts/search.js';
  5. import UserList from './userList.js';
  6. export async function renderTileHUD(hud, html, tileData, searchText = '', fp_files = null) {
  7. const tile = hud.object;
  8. const hudSettings = TVA_CONFIG.hud;
  9. if (!hudSettings.enableSideMenu || !TVA_CONFIG.tilesEnabled) return;
  10. const button = $(`
  11. <div class="control-icon" data-action="token-variants-side-selector">
  12. <img
  13. id="token-variants-side-button"
  14. src="modules/token-variants/img/token-images.svg"
  15. width="36"
  16. height="36"
  17. title="${game.i18n.localize('token-variants.windows.art-select.select-variant')}"
  18. />
  19. </div>
  20. `);
  21. html.find('div.right').last().append(button);
  22. html.find('div.right').click(_deactivateTokenVariantsSideSelector);
  23. button.click((event) => _onButtonClick(event, tile));
  24. button.contextmenu((event) => _onButtonRightClick(event, tile));
  25. }
  26. async function _onButtonClick(event, tile) {
  27. if (keyPressed('config')) {
  28. setNameDialog(tile);
  29. return;
  30. }
  31. const button = $(event.target).closest('.control-icon');
  32. // De-activate 'Status Effects'
  33. button.closest('div.right').find('div.control-icon.effects').removeClass('active');
  34. button.closest('div.right').find('.status-effects').removeClass('active');
  35. // Remove contextmenu
  36. button.find('.contextmenu').remove();
  37. // Toggle variants side menu
  38. button.toggleClass('active');
  39. let variantsWrap = button.find('.token-variants-wrap');
  40. if (button.hasClass('active')) {
  41. if (!variantsWrap.length) {
  42. variantsWrap = await renderSideSelect(tile);
  43. if (variantsWrap) button.find('img').after(variantsWrap);
  44. else return;
  45. }
  46. variantsWrap.addClass('active');
  47. } else {
  48. variantsWrap.removeClass('active');
  49. }
  50. }
  51. function _onButtonRightClick(event, tile) {
  52. // Display side menu if button is not active yet
  53. const button = $(event.target).closest('.control-icon');
  54. if (!button.hasClass('active')) {
  55. // button.trigger('click');
  56. button.addClass('active');
  57. }
  58. if (button.find('.contextmenu').length) {
  59. // Contextmenu already displayed. Remove and activate images
  60. button.find('.contextmenu').remove();
  61. button.removeClass('active').trigger('click');
  62. //button.find('.token-variants-wrap.images').addClass('active');
  63. } else {
  64. // Contextmenu is not displayed. Hide images, create it and add it
  65. button.find('.token-variants-wrap.images').removeClass('active');
  66. const contextMenu = $(`
  67. <div class="token-variants-wrap contextmenu active">
  68. <div class="token-variants-context-menu active">
  69. <input class="token-variants-side-search" type="text" />
  70. <button class="flags" type="button"><i class="fab fa-font-awesome-flag"></i><label>Flags</label></button>
  71. <button class="file-picker" type="button"><i class="fas fa-file-import fa-fw"></i><label>Browse Folders</label></button>
  72. </div>
  73. </div>
  74. `);
  75. button.append(contextMenu);
  76. // Register contextmenu listeners
  77. contextMenu
  78. .find('.token-variants-side-search')
  79. .on('keydown', (event) => _onImageSearchKeyUp(event, tile))
  80. .on('click', (event) => {
  81. event.preventDefault();
  82. event.stopPropagation();
  83. });
  84. contextMenu.find('.flags').click((event) => {
  85. event.preventDefault();
  86. event.stopPropagation();
  87. new FlagsConfig(tile).render(true);
  88. });
  89. contextMenu.find('.file-picker').click(async (event) => {
  90. event.preventDefault();
  91. event.stopPropagation();
  92. new FilePicker({
  93. type: 'folder',
  94. callback: async (path, fp) => {
  95. const content = await FilePicker.browse(fp.activeSource, fp.result.target);
  96. let files = content.files.filter((f) => isImage(f) || isVideo(f));
  97. if (files.length) {
  98. button.find('.token-variants-wrap').remove();
  99. const sideSelect = await renderSideSelect(tile, null, files);
  100. if (sideSelect) {
  101. sideSelect.addClass('active');
  102. button.append(sideSelect);
  103. }
  104. }
  105. },
  106. }).render(true);
  107. });
  108. }
  109. }
  110. function _deactivateTokenVariantsSideSelector(event) {
  111. const controlIcon = $(event.target).closest('.control-icon');
  112. const dataAction = controlIcon.attr('data-action');
  113. switch (dataAction) {
  114. case 'effects':
  115. break; // Effects button
  116. case 'thwildcard-selector':
  117. break; // Token HUD Wildcard module button
  118. default:
  119. return;
  120. }
  121. $(event.target)
  122. .closest('div.right')
  123. .find('.control-icon[data-action="token-variants-side-selector"]')
  124. .removeClass('active');
  125. $(event.target).closest('div.right').find('.token-variants-wrap').removeClass('active');
  126. }
  127. async function renderSideSelect(tile, searchText = null, fp_files = null) {
  128. const hudSettings = TVA_CONFIG.hud;
  129. const worldHudSettings = TVA_CONFIG.worldHud;
  130. const FULL_ACCESS = TVA_CONFIG.permissions.hudFullAccess[game.user.role];
  131. let images = [];
  132. let variants = [];
  133. let imageDuplicates = new Set();
  134. const pushImage = (img) => {
  135. if (imageDuplicates.has(img.path)) {
  136. if (!images.find((obj) => obj.path === img.path && obj.name === img.name)) {
  137. images.push(img);
  138. }
  139. } else {
  140. images.push(img);
  141. imageDuplicates.add(img.path);
  142. }
  143. };
  144. if (!fp_files) {
  145. if (searchText !== null && !searchText) return;
  146. if (!searchText) {
  147. variants = tile.document.getFlag('token-variants', 'variants') || [];
  148. variants.forEach((variant) => {
  149. for (const name of variant.names) {
  150. pushImage({ path: variant.imgSrc, name: name });
  151. }
  152. });
  153. // Parse directory flag and include the images
  154. const directoryFlag = tile.document.getFlag('token-variants', 'directory');
  155. if (directoryFlag) {
  156. let dirFlagImages;
  157. try {
  158. let path = directoryFlag.path;
  159. let source = directoryFlag.source;
  160. let bucket = '';
  161. if (source.startsWith('s3:')) {
  162. bucket = source.substring(3, source.length);
  163. source = 's3';
  164. }
  165. const content = await FilePicker.browse(source, path, {
  166. type: 'imagevideo',
  167. bucket,
  168. });
  169. dirFlagImages = content.files;
  170. } catch (err) {
  171. dirFlagImages = [];
  172. }
  173. dirFlagImages.forEach((f) => {
  174. if (isImage(f) || isVideo(f)) pushImage({ path: f, name: getFileName(f) });
  175. });
  176. }
  177. }
  178. // Perform the search if needed
  179. const search = searchText ?? tile.document.getFlag('token-variants', 'tileName');
  180. const noSearch = !search || (!searchText && worldHudSettings.displayOnlySharedImages);
  181. let artSearch = noSearch
  182. ? null
  183. : await doImageSearch(search, {
  184. searchType: SEARCH_TYPE.TILE,
  185. searchOptions: { keywordSearch: worldHudSettings.includeKeywords },
  186. });
  187. if (artSearch) {
  188. artSearch.forEach((results) => {
  189. images.push(...results);
  190. });
  191. }
  192. } else {
  193. images = fp_files.map((f) => {
  194. return { path: f, name: getFileName(f) };
  195. });
  196. }
  197. // Retrieving the possibly custom name attached as a flag to the token
  198. let tileImageName = tile.document.getFlag('token-variants', 'name');
  199. if (!tileImageName) {
  200. tileImageName = getFileName(tile.document.texture.src);
  201. }
  202. let imagesParsed = [];
  203. for (const imageObj of images) {
  204. const img = isImage(imageObj.path);
  205. const vid = isVideo(imageObj.path);
  206. let shared = false;
  207. if (game.user.isGM) {
  208. variants.forEach((variant) => {
  209. if (variant.imgSrc === imageObj.path && variant.names.includes(imageObj.name)) {
  210. shared = true;
  211. }
  212. });
  213. }
  214. const userMappings = tile.document.getFlag('token-variants', 'userMappings') || {};
  215. const [title, style] = genTitleAndStyle(userMappings, imageObj.path, imageObj.name);
  216. imagesParsed.push({
  217. route: imageObj.path,
  218. name: imageObj.name,
  219. used: imageObj.path === tile.document.texture.src && imageObj.name === tileImageName,
  220. img,
  221. vid,
  222. unknownType: !img && !vid,
  223. shared: shared,
  224. hasConfig: false, //hasConfig,
  225. title: title,
  226. style: game.user.isGM && style ? 'box-shadow: ' + style + ';' : null,
  227. });
  228. }
  229. //
  230. // Render
  231. //
  232. const imageDisplay = hudSettings.displayAsImage;
  233. const imageOpacity = hudSettings.imageOpacity / 100;
  234. const sideSelect = $(
  235. await renderTemplate('modules/token-variants/templates/sideSelect.html', {
  236. imagesParsed,
  237. imageDisplay,
  238. imageOpacity,
  239. autoplay: !TVA_CONFIG.playVideoOnHover,
  240. })
  241. );
  242. // Activate listeners
  243. sideSelect.find('video').hover(
  244. function () {
  245. if (TVA_CONFIG.playVideoOnHover) {
  246. this.play();
  247. $(this).siblings('.fa-play').hide();
  248. }
  249. },
  250. function () {
  251. if (TVA_CONFIG.pauseVideoOnHoverOut) {
  252. this.pause();
  253. this.currentTime = 0;
  254. $(this).siblings('.fa-play').show();
  255. }
  256. }
  257. );
  258. sideSelect.find('.token-variants-button-select').click((event) => _onImageClick(event, tile));
  259. if (FULL_ACCESS) {
  260. sideSelect
  261. .find('.token-variants-button-select')
  262. .on('contextmenu', (event) => _onImageRightClick(event, tile));
  263. }
  264. return sideSelect;
  265. }
  266. async function _onImageClick(event, tile) {
  267. event.preventDefault();
  268. event.stopPropagation();
  269. if (!tile) return;
  270. const imgButton = $(event.target).closest('.token-variants-button-select');
  271. const imgSrc = imgButton.attr('data-name');
  272. const name = imgButton.attr('data-filename');
  273. if (imgSrc) {
  274. canvas.tiles.hud.clear();
  275. await tile.document.update({ img: imgSrc });
  276. try {
  277. if (getFileName(imgSrc) !== name) await tile.document.setFlag('token-variants', 'name', name);
  278. } catch (e) {}
  279. }
  280. }
  281. async function _onImageRightClick(event, tile) {
  282. event.preventDefault();
  283. event.stopPropagation();
  284. if (!tile) return;
  285. const imgButton = $(event.target).closest('.token-variants-button-select');
  286. const imgSrc = imgButton.attr('data-name');
  287. const name = imgButton.attr('data-filename');
  288. if (!imgSrc || !name) return;
  289. if (keyPressed('config') && game.user.isGM) {
  290. const regenStyle = (tile, img) => {
  291. const mappings = tile.document.getFlag('token-variants', 'userMappings') || {};
  292. const name = imgButton.attr('data-filename');
  293. const [title, style] = genTitleAndStyle(mappings, img, name);
  294. imgButton
  295. .closest('.token-variants-wrap')
  296. .find(`.token-variants-button-select[data-name='${img}']`)
  297. .css('box-shadow', style)
  298. .prop('title', title);
  299. };
  300. new UserList(tile, imgSrc, regenStyle).render(true);
  301. return;
  302. }
  303. let variants = tile.document.getFlag('token-variants', 'variants') || [];
  304. // Remove selected variant if present in the flag, add otherwise
  305. let del = false;
  306. let updated = false;
  307. for (let variant of variants) {
  308. if (variant.imgSrc === imgSrc) {
  309. let fNames = variant.names.filter((name) => name !== name);
  310. if (fNames.length === 0) {
  311. del = true;
  312. } else if (fNames.length === variant.names.length) {
  313. fNames.push(name);
  314. }
  315. variant.names = fNames;
  316. updated = true;
  317. break;
  318. }
  319. }
  320. if (del) variants = variants.filter((variant) => variant.imgSrc !== imgSrc);
  321. else if (!updated) variants.push({ imgSrc: imgSrc, names: [name] });
  322. // Set shared variants as a flag
  323. tile.document.unsetFlag('token-variants', 'variants');
  324. if (variants.length > 0) {
  325. tile.document.setFlag('token-variants', 'variants', variants);
  326. }
  327. imgButton.find('.fa-share').toggleClass('active'); // Display green arrow
  328. }
  329. async function _onImageSearchKeyUp(event, tile) {
  330. if (event.key === 'Enter' || event.keyCode === 13) {
  331. event.preventDefault();
  332. if (event.target.value.length >= 3) {
  333. const button = $(event.target).closest('.control-icon');
  334. button.find('.token-variants-wrap').remove();
  335. const sideSelect = await renderSideSelect(tile, event.target.value);
  336. if (sideSelect) {
  337. sideSelect.addClass('active');
  338. button.append(sideSelect);
  339. }
  340. }
  341. return false;
  342. }
  343. }
  344. function genTitleAndStyle(mappings, imgSrc, name) {
  345. let title = TVA_CONFIG.worldHud.showFullPath ? imgSrc : name;
  346. let style = '';
  347. let offset = 2;
  348. for (const [userId, img] of Object.entries(mappings)) {
  349. if (img === imgSrc) {
  350. const user = game.users.get(userId);
  351. if (!user) continue;
  352. if (style.length === 0) {
  353. style = `inset 0 0 0 ${offset}px ${user.color}`;
  354. } else {
  355. style += `, inset 0 0 0 ${offset}px ${user.color}`;
  356. }
  357. offset += 2;
  358. title += `\nDisplayed to: ${user.name}`;
  359. }
  360. }
  361. return [title, style];
  362. }
  363. function setNameDialog(tile) {
  364. const tileName = tile.document.getFlag('token-variants', 'tileName') || tile.id;
  365. new Dialog({
  366. title: `Assign a name to the Tile (3+ chars)`,
  367. 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>`,
  368. buttons: {
  369. Ok: {
  370. label: `Save`,
  371. callback: (html) => {
  372. const name = html.find('input').val();
  373. if (name) {
  374. canvas.tiles.hud.clear();
  375. tile.document.setFlag('token-variants', 'tileName', name);
  376. }
  377. },
  378. },
  379. },
  380. }).render(true);
  381. }