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.

458 lines
14 KiB

  1. import TokenCustomConfig from './tokenCustomConfig.js';
  2. import { isVideo, isImage, keyPressed, SEARCH_TYPE, BASE_IMAGE_CATEGORIES, getFileName } from '../scripts/utils.js';
  3. import { showArtSelect } from '../token-variants.mjs';
  4. import { TVA_CONFIG, getSearchOptions } from '../scripts/settings.js';
  5. const ART_SELECT_QUEUE = {
  6. queue: [],
  7. };
  8. export function addToArtSelectQueue(search, options) {
  9. ART_SELECT_QUEUE.queue.push({
  10. search: search,
  11. options: options,
  12. });
  13. $('button#token-variant-art-clear-queue').html(`Clear Queue (${ART_SELECT_QUEUE.queue.length})`).show();
  14. }
  15. export function addToQueue(search, options) {
  16. ART_SELECT_QUEUE.queue.push({
  17. search: search,
  18. options: options,
  19. });
  20. }
  21. export function renderFromQueue(force = false) {
  22. if (!force) {
  23. const artSelects = Object.values(ui.windows).filter((app) => app instanceof ArtSelect);
  24. if (artSelects.length !== 0) {
  25. if (ART_SELECT_QUEUE.queue.length !== 0)
  26. $('button#token-variant-art-clear-queue').html(`Clear Queue (${ART_SELECT_QUEUE.queue.length})`).show();
  27. return;
  28. }
  29. }
  30. let callData = ART_SELECT_QUEUE.queue.shift();
  31. if (callData?.options.execute) {
  32. callData.options.execute();
  33. callData = ART_SELECT_QUEUE.queue.shift();
  34. }
  35. if (callData) {
  36. showArtSelect(callData.search, callData.options);
  37. }
  38. }
  39. function delay(fn, ms) {
  40. let timer = 0;
  41. return function (...args) {
  42. clearTimeout(timer);
  43. timer = setTimeout(fn.bind(this, ...args), ms || 0);
  44. };
  45. }
  46. export class ArtSelect extends FormApplication {
  47. static instance = null;
  48. static IMAGE_DISPLAY = {
  49. NONE: 0,
  50. PORTRAIT: 1,
  51. TOKEN: 2,
  52. PORTRAIT_TOKEN: 3,
  53. IMAGE: 4,
  54. };
  55. constructor(
  56. search,
  57. {
  58. preventClose = false,
  59. object = null,
  60. callback = null,
  61. searchType = null,
  62. allImages = null,
  63. image1 = '',
  64. image2 = '',
  65. displayMode = ArtSelect.IMAGE_DISPLAY.NONE,
  66. multipleSelection = false,
  67. searchOptions = {},
  68. } = {}
  69. ) {
  70. let title = game.i18n.localize('token-variants.windows.art-select.select-variant');
  71. if (searchType === SEARCH_TYPE.TOKEN)
  72. title = game.i18n.localize('token-variants.windows.art-select.select-token-art');
  73. else if (searchType === SEARCH_TYPE.PORTRAIT)
  74. title = game.i18n.localize('token-variants.windows.art-select.select-portrait-art');
  75. super(
  76. {},
  77. {
  78. closeOnSubmit: false,
  79. width: ArtSelect.WIDTH || 500,
  80. height: ArtSelect.HEIGHT || 500,
  81. left: ArtSelect.LEFT,
  82. top: ArtSelect.TOP,
  83. title: title,
  84. }
  85. );
  86. this.search = search;
  87. this.allImages = allImages;
  88. this.callback = callback;
  89. this.doc = object;
  90. this.preventClose = preventClose;
  91. this.image1 = image1;
  92. this.image2 = image2;
  93. this.displayMode = displayMode;
  94. this.multipleSelection = multipleSelection;
  95. this.searchType = searchType;
  96. this.searchOptions = mergeObject(searchOptions, getSearchOptions(), {
  97. overwrite: false,
  98. });
  99. ArtSelect.instance = this;
  100. }
  101. static get defaultOptions() {
  102. return mergeObject(super.defaultOptions, {
  103. id: 'token-variants-art-select',
  104. classes: ['sheet'],
  105. template: 'modules/token-variants/templates/artSelect.html',
  106. resizable: true,
  107. minimizable: false,
  108. });
  109. }
  110. _getHeaderButtons() {
  111. const buttons = super._getHeaderButtons();
  112. buttons.unshift({
  113. label: 'FilePicker',
  114. class: 'file-picker',
  115. icon: 'fas fa-file-import fa-fw',
  116. onclick: () => {
  117. new FilePicker({
  118. type: 'imagevideo',
  119. callback: (path) => {
  120. if (!this.preventClose) {
  121. this.close();
  122. }
  123. if (this.callback) {
  124. this.callback(path, getFileName(path));
  125. }
  126. },
  127. }).render();
  128. },
  129. });
  130. buttons.unshift({
  131. label: 'Image Category',
  132. class: 'type',
  133. icon: 'fas fa-swatchbook',
  134. onclick: () => {
  135. if (ArtSelect.instance) ArtSelect.instance._typeSelect();
  136. },
  137. });
  138. return buttons;
  139. }
  140. _typeSelect() {
  141. const categories = BASE_IMAGE_CATEGORIES.concat(TVA_CONFIG.customImageCategories);
  142. const buttons = {};
  143. for (const c of categories) {
  144. let label = c;
  145. if (c === this.searchType) {
  146. label = '<b>>>> ' + label + ' <<<</b>';
  147. }
  148. buttons[c] = {
  149. label: label,
  150. callback: () => {
  151. if (this.searchType !== c) {
  152. this.searchType = c;
  153. this._performSearch(this.search, true);
  154. }
  155. },
  156. };
  157. }
  158. new Dialog({
  159. title: `Select Image Category and Filter`,
  160. content: `<style>.dialog .dialog-button {flex: 0 0 auto;}</style>`,
  161. buttons: buttons,
  162. }).render(true);
  163. }
  164. async getData(options) {
  165. const data = super.getData(options);
  166. if (this.doc instanceof Item) {
  167. data.item = true;
  168. data.description = this.doc.system?.description?.value ?? '';
  169. }
  170. const searchOptions = this.searchOptions;
  171. const algorithm = searchOptions.algorithm;
  172. //
  173. // Create buttons
  174. //
  175. const tokenConfigs = (TVA_CONFIG.tokenConfigs || []).flat();
  176. const fuzzySearch = algorithm.fuzzy;
  177. let allButtons = new Map();
  178. let artFound = false;
  179. const genLabel = function (str, indices, start = '<mark>', end = '</mark>', fillChar = null) {
  180. if (!indices) return str;
  181. let fillStr = fillChar ? fillChar.repeat(str.length) : str;
  182. let label = '';
  183. let lastIndex = 0;
  184. for (const index of indices) {
  185. label += fillStr.slice(lastIndex, index[0]);
  186. label += start + str.slice(index[0], index[1] + 1) + end;
  187. lastIndex = index[1] + 1;
  188. }
  189. label += fillStr.slice(lastIndex, fillStr.length);
  190. return label;
  191. };
  192. const genTitle = function (obj) {
  193. if (!fuzzySearch) return obj.path;
  194. let percent = Math.ceil((1 - obj.score) * 100) + '%';
  195. if (searchOptions.runSearchOnPath) {
  196. return percent + '\n' + genLabel(obj.path, obj.indices, '', '', '-') + '\n' + obj.path;
  197. }
  198. return percent;
  199. };
  200. this.allImages.forEach((images, search) => {
  201. const buttons = [];
  202. images.forEach((imageObj) => {
  203. artFound = true;
  204. const vid = isVideo(imageObj.path);
  205. const img = isImage(imageObj.path);
  206. buttons.push({
  207. path: imageObj.path,
  208. img: img,
  209. vid: vid,
  210. type: vid || img,
  211. name: imageObj.name,
  212. label:
  213. fuzzySearch && !searchOptions.runSearchOnPath ? genLabel(imageObj.name, imageObj.indices) : imageObj.name,
  214. title: genTitle(imageObj),
  215. hasConfig:
  216. this.searchType === SEARCH_TYPE.TOKEN || this.searchType === SEARCH_TYPE.PORTRAIT_AND_TOKEN
  217. ? Boolean(
  218. tokenConfigs.find((config) => config.tvImgSrc == imageObj.path && config.tvImgName == imageObj.name)
  219. )
  220. : false,
  221. });
  222. });
  223. allButtons.set(search, buttons);
  224. });
  225. if (artFound) data.allImages = allButtons;
  226. data.search = this.search;
  227. data.queue = ART_SELECT_QUEUE.queue.length;
  228. data.image1 = this.image1;
  229. data.image2 = this.image2;
  230. data.displayMode = this.displayMode;
  231. data.multipleSelection = this.multipleSelection;
  232. data.displaySlider = algorithm.fuzzy && algorithm.fuzzyArtSelectPercentSlider;
  233. data.fuzzyThreshold = algorithm.fuzzyThreshold;
  234. if (data.displaySlider) {
  235. data.fuzzyThreshold = 100 - data.fuzzyThreshold * 100;
  236. data.fuzzyThreshold = data.fuzzyThreshold.toFixed(0);
  237. }
  238. data.autoplay = !TVA_CONFIG.playVideoOnHover;
  239. return data;
  240. }
  241. /**
  242. * @param {JQuery} html
  243. */
  244. activateListeners(html) {
  245. super.activateListeners(html);
  246. const callback = this.callback;
  247. const close = () => this.close();
  248. const object = this.doc;
  249. const preventClose = this.preventClose;
  250. const multipleSelection = this.multipleSelection;
  251. const boxes = html.find(`.token-variants-grid-box`);
  252. boxes.hover(
  253. function () {
  254. if (TVA_CONFIG.playVideoOnHover) {
  255. const vid = $(this).siblings('video');
  256. if (vid.length) {
  257. vid[0].play();
  258. $(this).siblings('.fa-play').hide();
  259. }
  260. }
  261. },
  262. function () {
  263. if (TVA_CONFIG.pauseVideoOnHoverOut) {
  264. const vid = $(this).siblings('video');
  265. if (vid.length) {
  266. vid[0].pause();
  267. vid[0].currentTime = 0;
  268. $(this).siblings('.fa-play').show();
  269. }
  270. }
  271. }
  272. );
  273. boxes.map((box) => {
  274. boxes[box].addEventListener('click', async function (event) {
  275. if (keyPressed('config')) {
  276. if (object)
  277. new TokenCustomConfig(object, {}, event.target.dataset.name, event.target.dataset.filename).render(true);
  278. } else {
  279. if (!preventClose) {
  280. close();
  281. }
  282. if (callback) {
  283. callback(event.target.dataset.name, event.target.dataset.filename);
  284. }
  285. }
  286. });
  287. if (multipleSelection) {
  288. boxes[box].addEventListener('contextmenu', async function (event) {
  289. $(event.target).toggleClass('selected');
  290. });
  291. }
  292. });
  293. let searchInput = html.find('#custom-art-search');
  294. searchInput.focus();
  295. searchInput[0].selectionStart = searchInput[0].selectionEnd = 10000;
  296. searchInput.on(
  297. 'input',
  298. delay((event) => {
  299. this._performSearch(event.target.value);
  300. }, 350)
  301. );
  302. html.find(`button#token-variant-art-clear-queue`).on('click', (event) => {
  303. ART_SELECT_QUEUE.queue = ART_SELECT_QUEUE.queue.filter((callData) => callData.options.execute);
  304. $(event.target).hide();
  305. });
  306. $(html)
  307. .find('[name="fuzzyThreshold"]')
  308. .change((e) => {
  309. $(e.target)
  310. .siblings('.token-variants-range-value')
  311. .html(`${parseFloat(e.target.value).toFixed(0)}%`);
  312. this.searchOptions.algorithm.fuzzyThreshold = (100 - e.target.value) / 100;
  313. })
  314. .change(
  315. delay((event) => {
  316. this._performSearch(this.search, true);
  317. }, 350)
  318. );
  319. if (multipleSelection) {
  320. html.find(`button#token-variant-art-return-selected`).on('click', () => {
  321. if (callback) {
  322. const images = [];
  323. html
  324. .find(`.token-variants-grid-box.selected`)
  325. .siblings('.token-variants-grid-image')
  326. .each(function () {
  327. images.push(this.getAttribute('src'));
  328. });
  329. callback(images);
  330. }
  331. close();
  332. });
  333. html.find(`button#token-variant-art-return-all`).on('click', () => {
  334. if (callback) {
  335. const images = [];
  336. html.find(`.token-variants-grid-image`).each(function () {
  337. images.push(this.getAttribute('src'));
  338. });
  339. callback(images);
  340. }
  341. close();
  342. });
  343. }
  344. }
  345. _performSearch(search, force = false) {
  346. if (!force && this.search.trim() === search.trim()) return;
  347. showArtSelect(search, {
  348. callback: this.callback,
  349. searchType: this.searchType,
  350. object: this.doc,
  351. force: true,
  352. image1: this.image1,
  353. image2: this.image2,
  354. displayMode: this.displayMode,
  355. multipleSelection: this.multipleSelection,
  356. searchOptions: this.searchOptions,
  357. preventClose: this.preventClose,
  358. });
  359. }
  360. /**
  361. * @param {Event} event
  362. * @param {Object} formData
  363. */
  364. async _updateObject(event, formData) {
  365. if (formData && formData.search != this.search) {
  366. this._performSearch(formData.search);
  367. } else {
  368. this.close();
  369. }
  370. }
  371. setPosition(options = {}) {
  372. if (options.width) ArtSelect.WIDTH = options.width;
  373. if (options.height) ArtSelect.HEIGHT = options.height;
  374. if (options.top) ArtSelect.TOP = options.top;
  375. if (options.left) ArtSelect.LEFT = options.left;
  376. super.setPosition(options);
  377. }
  378. async close(options = {}) {
  379. let callData = ART_SELECT_QUEUE.queue.shift();
  380. if (callData?.options.execute) {
  381. callData.options.execute();
  382. callData = ART_SELECT_QUEUE.queue.shift();
  383. }
  384. if (callData) {
  385. callData.options.force = true;
  386. showArtSelect(callData.search, callData.options);
  387. } else {
  388. // For some reason there might be app instances that have not closed themselves by this point
  389. // If there are, close them now
  390. const artSelects = Object.values(ui.windows)
  391. .filter((app) => app instanceof ArtSelect)
  392. .filter((app) => app.appId !== this.appId);
  393. for (const app of artSelects) {
  394. app.close();
  395. }
  396. return super.close(options);
  397. }
  398. }
  399. }
  400. export function insertArtSelectButton(html, target, { search = '', searchType = SEARCH_TYPE.TOKEN } = {}) {
  401. const button = $(`<button
  402. class="token-variants-image-select-button"
  403. type="button"
  404. data-type="imagevideo"
  405. data-target="${target}"
  406. title="${game.i18n.localize('token-variants.windows.art-select.select-variant')}">
  407. <i class="fas fa-images"></i>
  408. </button>`);
  409. button.on('click', () => {
  410. showArtSelect(search, {
  411. callback: (imgSrc, name) => {
  412. button.siblings(`[name="${target}"]`).val(imgSrc);
  413. },
  414. searchType,
  415. });
  416. });
  417. const input = html.find(`[name="${target}"]`);
  418. input.after(button);
  419. return Boolean(input.length);
  420. }