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.

804 lines
27 KiB

  1. import { cacheImages } from '../scripts/search.js';
  2. import { TVA_CONFIG, updateSettings } from '../scripts/settings.js';
  3. import { getFileName } from '../scripts/utils.js';
  4. import EffectMappingForm from './effectMappingForm.js';
  5. import { showPathSelectCategoryDialog, showPathSelectConfigForm } from './dialogs.js';
  6. export default class ConfigureSettings extends FormApplication {
  7. constructor(
  8. dummySettings,
  9. {
  10. searchPaths = true,
  11. searchFilters = true,
  12. searchAlgorithm = true,
  13. randomizer = true,
  14. popup = true,
  15. permissions = true,
  16. worldHud = true,
  17. misc = true,
  18. activeEffects = true,
  19. features = false,
  20. } = {}
  21. ) {
  22. super({}, {});
  23. this.enabledTabs = {
  24. searchPaths,
  25. searchFilters,
  26. searchAlgorithm,
  27. randomizer,
  28. features,
  29. popup,
  30. permissions,
  31. worldHud,
  32. misc,
  33. activeEffects,
  34. };
  35. this.settings = foundry.utils.deepClone(TVA_CONFIG);
  36. if (dummySettings) {
  37. this.settings = mergeObject(this.settings, dummySettings, { insertKeys: false });
  38. this.dummySettings = dummySettings;
  39. }
  40. }
  41. static get defaultOptions() {
  42. return mergeObject(super.defaultOptions, {
  43. id: 'token-variants-configure-settings',
  44. classes: ['sheet'],
  45. template: 'modules/token-variants/templates/configureSettings.html',
  46. resizable: false,
  47. minimizable: false,
  48. title: 'Configure Settings',
  49. width: 700,
  50. height: 'auto',
  51. tabs: [{ navSelector: '.sheet-tabs', contentSelector: '.content', initial: 'searchPaths' }],
  52. });
  53. }
  54. async getData(options) {
  55. const data = super.getData(options);
  56. const settings = this.settings;
  57. data.enabledTabs = this.enabledTabs;
  58. // === Search Paths ===
  59. const paths = settings.searchPaths.map((path) => {
  60. const r = {};
  61. r.text = path.text;
  62. r.icon = this._pathIcon(path.source || '');
  63. r.cache = path.cache;
  64. r.source = path.source || '';
  65. r.types = path.types.join(',');
  66. r.config = JSON.stringify(path.config ?? {});
  67. r.hasConfig = path.config && !isEmpty(path.config);
  68. return r;
  69. });
  70. data.searchPaths = paths;
  71. // === Search Filters ===
  72. data.searchFilters = settings.searchFilters;
  73. for (const filter in data.searchFilters) {
  74. data.searchFilters[filter].label = filter;
  75. }
  76. // === Algorithm ===
  77. data.algorithm = deepClone(settings.algorithm);
  78. data.algorithm.fuzzyThreshold = 100 - data.algorithm.fuzzyThreshold * 100;
  79. // === Randomizer ===
  80. // Get all actor types defined by the game system
  81. data.randomizer = deepClone(settings.randomizer);
  82. const actorTypes = (game.system.entityTypes ?? game.system.documentTypes)['Actor'];
  83. data.randomizer.actorTypes = actorTypes.reduce((obj, t) => {
  84. const label = CONFIG['Actor']?.typeLabels?.[t] ?? t;
  85. obj[t] = {
  86. label: game.i18n.has(label) ? game.i18n.localize(label) : t,
  87. disable: settings.randomizer[`${t}Disable`] ?? false,
  88. };
  89. return obj;
  90. }, {});
  91. data.randomizer.tokenToPortraitDisabled =
  92. !(settings.randomizer.tokenCreate || settings.randomizer.tokenCopyPaste) ||
  93. data.randomizer.diffImages;
  94. // === Pop-up ===
  95. data.popup = deepClone(settings.popup);
  96. // Get all actor types defined by the game system
  97. data.popup.actorTypes = actorTypes.reduce((obj, t) => {
  98. const label = CONFIG['Actor']?.typeLabels?.[t] ?? t;
  99. obj[t] = {
  100. type: t,
  101. label: game.i18n.has(label) ? game.i18n.localize(label) : t,
  102. disable: settings.popup[`${t}Disable`] ?? false,
  103. };
  104. return obj;
  105. }, {});
  106. // Split into arrays of max length 3
  107. let allTypes = [];
  108. let tempTypes = [];
  109. let i = 0;
  110. for (const [key, value] of Object.entries(data.popup.actorTypes)) {
  111. tempTypes.push(value);
  112. i++;
  113. if (i % 3 == 0) {
  114. allTypes.push(tempTypes);
  115. tempTypes = [];
  116. }
  117. }
  118. if (tempTypes.length > 0) allTypes.push(tempTypes);
  119. data.popup.actorTypes = allTypes;
  120. // === Permissions ===
  121. data.permissions = settings.permissions;
  122. // === Token HUD ===
  123. data.worldHud = deepClone(settings.worldHud);
  124. data.worldHud.tokenHUDWildcardActive = game.modules.get('token-hud-wildcard')?.active;
  125. // === Internal Effects ===
  126. data.internalEffects = deepClone(settings.internalEffects);
  127. // === Misc ===
  128. data.keywordSearch = settings.keywordSearch;
  129. data.excludedKeywords = settings.excludedKeywords;
  130. data.systemHpPath = settings.systemHpPath;
  131. data.runSearchOnPath = settings.runSearchOnPath;
  132. data.imgurClientId = settings.imgurClientId;
  133. data.enableStatusConfig = settings.enableStatusConfig;
  134. data.disableNotifs = settings.disableNotifs;
  135. data.staticCache = settings.staticCache;
  136. data.staticCacheFile = settings.staticCacheFile;
  137. data.stackStatusConfig = settings.stackStatusConfig;
  138. data.mergeGroup = settings.mergeGroup;
  139. data.customImageCategories = settings.customImageCategories.join(',');
  140. data.disableEffectIcons = settings.disableEffectIcons;
  141. data.displayEffectIconsOnHover = settings.displayEffectIconsOnHover;
  142. data.filterEffectIcons = settings.filterEffectIcons;
  143. data.hideElevationTooltip = settings.hideElevationTooltip;
  144. data.hideTokenBorder = settings.hideTokenBorder;
  145. data.filterCustomEffectIcons = settings.filterCustomEffectIcons;
  146. data.filterIconList = settings.filterIconList.join(',');
  147. data.tilesEnabled = settings.tilesEnabled;
  148. data.updateTokenProto = settings.updateTokenProto;
  149. data.imgNameContainsDimensions = settings.imgNameContainsDimensions;
  150. data.imgNameContainsFADimensions = settings.imgNameContainsFADimensions;
  151. data.playVideoOnHover = settings.playVideoOnHover;
  152. data.pauseVideoOnHoverOut = settings.pauseVideoOnHoverOut;
  153. data.disableImageChangeOnPolymorphed = settings.disableImageChangeOnPolymorphed;
  154. data.disableImageUpdateOnNonPrototype = settings.disableImageUpdateOnNonPrototype;
  155. data.disableTokenUpdateAnimation = settings.disableTokenUpdateAnimation;
  156. data.mappingsCurrentSceneOnly = settings.mappingsCurrentSceneOnly;
  157. // Controls
  158. data.pathfinder = ['pf1e', 'pf2e'].includes(game.system.id);
  159. data.dnd5e = game.system.id === 'dnd5e';
  160. return data;
  161. }
  162. /**
  163. * @param {JQuery} html
  164. */
  165. activateListeners(html) {
  166. super.activateListeners(html);
  167. // Search Paths
  168. super.activateListeners(html);
  169. html.find('a.create-path').click(this._onCreatePath.bind(this));
  170. html.on('input', '.searchSource', this._onSearchSourceTextChange.bind(this));
  171. $(html).on('click', 'a.delete-path', this._onDeletePath.bind(this));
  172. $(html).on('click', 'a.convert-imgur', this._onConvertImgurPath.bind(this));
  173. $(html).on('click', 'a.convert-json', this._onConvertJsonPath.bind(this));
  174. $(html).on('click', '.path-image.source-icon a', this._onBrowseFolder.bind(this));
  175. $(html).on('click', 'a.select-category', showPathSelectCategoryDialog.bind(this));
  176. $(html).on('click', 'a.select-config', showPathSelectConfigForm.bind(this));
  177. // Search Filters
  178. html.on('input', 'input.filterRegex', this._validateRegex.bind(this));
  179. // Active Effects
  180. const disableEffectIcons = html.find('[name="disableEffectIcons"]');
  181. const filterEffectIcons = html.find('[name="filterEffectIcons"]');
  182. disableEffectIcons
  183. .on('change', (e) => {
  184. if (e.target.checked) filterEffectIcons.prop('checked', false);
  185. })
  186. .trigger('change');
  187. filterEffectIcons.on('change', (e) => {
  188. if (e.target.checked) disableEffectIcons.prop('checked', false);
  189. });
  190. // Algorithm
  191. const algorithmTab = $(html).find('div[data-tab="searchAlgorithm"]');
  192. algorithmTab.find(`input[name="algorithm.exact"]`).change((e) => {
  193. $(e.target)
  194. .closest('form')
  195. .find('input[name="algorithm.fuzzy"]')
  196. .prop('checked', !e.target.checked);
  197. });
  198. algorithmTab.find(`input[name="algorithm.fuzzy"]`).change((e) => {
  199. $(e.target)
  200. .closest('form')
  201. .find('input[name="algorithm.exact"]')
  202. .prop('checked', !e.target.checked);
  203. });
  204. algorithmTab.find('input[name="algorithm.fuzzyThreshold"]').change((e) => {
  205. $(e.target).siblings('.token-variants-range-value').html(`${e.target.value}%`);
  206. });
  207. // Randomizer
  208. const tokenCreate = html.find('input[name="randomizer.tokenCreate"]');
  209. const tokenCopyPaste = html.find('input[name="randomizer.tokenCopyPaste"]');
  210. const tokenToPortrait = html.find('input[name="randomizer.tokenToPortrait"]');
  211. const _toggle = () => {
  212. tokenToPortrait.prop(
  213. 'disabled',
  214. !(tokenCreate.is(':checked') || tokenCopyPaste.is(':checked'))
  215. );
  216. };
  217. tokenCreate.change(_toggle);
  218. tokenCopyPaste.change(_toggle);
  219. const diffImages = html.find('input[name="randomizer.diffImages"]');
  220. const syncImages = html.find('input[name="randomizer.syncImages"]');
  221. diffImages.change(() => {
  222. syncImages.prop('disabled', !diffImages.is(':checked'));
  223. tokenToPortrait.prop('disabled', diffImages.is(':checked'));
  224. });
  225. // Token HUD
  226. html.find('input[name="worldHud.updateActorImage"]').change((event) => {
  227. $(event.target)
  228. .closest('form')
  229. .find('input[name="worldHud.useNameSimilarity"]')
  230. .prop('disabled', !event.target.checked);
  231. });
  232. // Static Cache
  233. html.find('button.token-variants-cache-images').click((event) => {
  234. const tab = $(event.target).closest('.tab');
  235. const staticOn = tab.find('input[name="staticCache"]');
  236. const staticFile = tab.find('input[name="staticCacheFile"]');
  237. cacheImages({ staticCache: staticOn.is(':checked'), staticCacheFile: staticFile.val() });
  238. });
  239. // Global Mappings
  240. html.find('button.token-variants-global-mapping').click(() => {
  241. const setting = game.settings.get('core', DefaultTokenConfig.SETTING);
  242. const data = new foundry.data.PrototypeToken(setting);
  243. const token = new TokenDocument(data, { actor: null });
  244. new EffectMappingForm(token, { globalMappings: true }).render(true);
  245. });
  246. }
  247. /**
  248. * Validates regex entered into Search Filter's RegEx input field
  249. */
  250. async _validateRegex(event) {
  251. if (this._validRegex(event.target.value)) {
  252. event.target.style.backgroundColor = '';
  253. } else {
  254. event.target.style.backgroundColor = '#ff7066';
  255. }
  256. }
  257. _validRegex(val) {
  258. if (val) {
  259. try {
  260. new RegExp(val);
  261. } catch (e) {
  262. return false;
  263. }
  264. }
  265. return true;
  266. }
  267. /**
  268. * Open a FilePicker so the user can select a local folder to use as an image source
  269. */
  270. async _onBrowseFolder(event) {
  271. const pathInput = $(event.target).closest('.table-row').find('.path-text input');
  272. const sourceInput = $(event.target).closest('.table-row').find('.path-source input');
  273. let activeSource = sourceInput.val() || 'data';
  274. let current = pathInput.val();
  275. if (activeSource.startsWith('s3:')) {
  276. const bucketName = activeSource.replace('s3:', '');
  277. current = `${game.data.files.s3?.endpoint.protocol}//${bucketName}.${game.data.files.s3?.endpoint.host}/${current}`;
  278. } else if (activeSource.startsWith('rolltable')) {
  279. let content = `<select style="width: 100%;" name="table-name" id="output-tableKey">`;
  280. game.tables.forEach((rollTable) => {
  281. content += `<option value='${rollTable.name}'>${rollTable.name}</option>`;
  282. });
  283. content += `</select>`;
  284. new Dialog({
  285. title: `Select a Rolltable`,
  286. content: content,
  287. buttons: {
  288. yes: {
  289. icon: "<i class='fas fa-check'></i>",
  290. label: 'Select',
  291. callback: (html) => {
  292. pathInput.val();
  293. const tableName = html.find("select[name='table-name']").val();
  294. pathInput.val(tableName);
  295. },
  296. },
  297. },
  298. default: 'yes',
  299. }).render(true);
  300. return;
  301. }
  302. if (activeSource === 'json') {
  303. new FilePicker({
  304. type: 'text',
  305. activeSource: 'data',
  306. current: current,
  307. callback: (path, fp) => {
  308. pathInput.val(path);
  309. },
  310. }).render(true);
  311. } else {
  312. new FilePicker({
  313. type: 'folder',
  314. activeSource: activeSource,
  315. current: current,
  316. callback: (path, fp) => {
  317. pathInput.val(fp.result.target);
  318. if (fp.activeSource === 's3') {
  319. sourceInput.val(`s3:${fp.result.bucket}`);
  320. } else {
  321. sourceInput.val(fp.activeSource);
  322. }
  323. },
  324. }).render(true);
  325. }
  326. }
  327. /**
  328. * Converts Imgur path to a rolltable
  329. */
  330. async _onConvertImgurPath(event) {
  331. event.preventDefault();
  332. const pathInput = $(event.target).closest('.table-row').find('.path-text input');
  333. const sourceInput = $(event.target).closest('.table-row').find('.path-source input');
  334. const albumHash = pathInput.val();
  335. const imgurClientId =
  336. TVA_CONFIG.imgurClientId === '' ? 'df9d991443bb222' : TVA_CONFIG.imgurClientId;
  337. fetch('https://api.imgur.com/3/gallery/album/' + albumHash, {
  338. headers: {
  339. Authorization: 'Client-ID ' + imgurClientId,
  340. Accept: 'application/json',
  341. },
  342. })
  343. .then((response) => response.json())
  344. .then(
  345. async function (result) {
  346. if (!result.success && location.hostname === 'localhost') {
  347. ui.notifications.warn(
  348. game.i18n.format('token-variants.notifications.warn.imgur-localhost')
  349. );
  350. return;
  351. }
  352. const data = result.data;
  353. let resultsArray = [];
  354. data.images.forEach((img, i) => {
  355. resultsArray.push({
  356. type: 0,
  357. text: img.title ?? img.description ?? '',
  358. weight: 1,
  359. range: [i + 1, i + 1],
  360. collection: 'Text',
  361. drawn: false,
  362. img: img.link,
  363. });
  364. });
  365. await RollTable.create({
  366. name: data.title,
  367. description:
  368. 'Token Variant Art auto generated RollTable: https://imgur.com/gallery/' + albumHash,
  369. results: resultsArray,
  370. replacement: true,
  371. displayRoll: true,
  372. img: 'modules/token-variants/img/token-images.svg',
  373. });
  374. pathInput.val(data.title);
  375. sourceInput.val('rolltable').trigger('input');
  376. }.bind(this)
  377. )
  378. .catch((error) => console.warn('TVA | ', error));
  379. }
  380. /**
  381. * Converts Json path to a rolltable
  382. */
  383. async _onConvertJsonPath(event) {
  384. event.preventDefault();
  385. const pathInput = $(event.target).closest('.table-row').find('.path-text input');
  386. const sourceInput = $(event.target).closest('.table-row').find('.path-source input');
  387. const jsonPath = pathInput.val();
  388. fetch(jsonPath, {
  389. headers: {
  390. Accept: 'application/json',
  391. },
  392. })
  393. .then((response) => response.json())
  394. .then(
  395. async function (result) {
  396. if (!result.length > 0) {
  397. ui.notifications.warn(
  398. game.i18n.format('token-variants.notifications.warn.json-localhost')
  399. );
  400. return;
  401. }
  402. const data = result;
  403. data.title = getFileName(jsonPath);
  404. let resultsArray = [];
  405. data.forEach((img, i) => {
  406. resultsArray.push({
  407. type: 0,
  408. text: img.name ?? '',
  409. weight: 1,
  410. range: [i + 1, i + 1],
  411. collection: 'Text',
  412. drawn: false,
  413. img: img.path,
  414. });
  415. });
  416. await RollTable.create({
  417. name: data.title,
  418. description: 'Token Variant Art auto generated RollTable: ' + jsonPath,
  419. results: resultsArray,
  420. replacement: true,
  421. displayRoll: true,
  422. img: 'modules/token-variants/img/token-images.svg',
  423. });
  424. pathInput.val(data.title);
  425. sourceInput.val('rolltable').trigger('input');
  426. }.bind(this)
  427. )
  428. .catch((error) => console.warn('TVA | ', error));
  429. }
  430. /**
  431. * Generates a new search path row
  432. */
  433. async _onCreatePath(event) {
  434. event.preventDefault();
  435. const table = $(event.currentTarget).closest('.token-variant-table');
  436. let row = `
  437. <li class="table-row flexrow">
  438. <div class="path-image source-icon">
  439. <a><i class="${this._pathIcon('')}"></i></a>
  440. </div>
  441. <div class="path-source">
  442. <input class="searchSource" type="text" name="searchPaths.source" value="" placeholder="data"/>
  443. </div>
  444. <div class="path-text">
  445. <input class="searchPath" type="text" name="searchPaths.text" value="" placeholder="Path to folder"/>
  446. </div>
  447. <div class="imgur-control">
  448. <a class="convert-imgur" title="Convert to Rolltable"><i class="fas fa-angle-double-left"></i></a>
  449. </div>
  450. <div class="json-control">
  451. <a class="convert-json" title="Convert to Rolltable"><i class="fas fa-angle-double-left"></i></a>
  452. </div>
  453. <div class="path-category">
  454. <a class="select-category" title="Select image categories/filters"><i class="fas fa-swatchbook"></i></a>
  455. <input type="hidden" name="searchPaths.types" value="Portrait,Token,PortraitAndToken">
  456. </div>
  457. <div class="path-config">
  458. <a class="select-config" title="Apply configuration to images under this path."><i class="fas fa-cog fa-lg"></i></a>
  459. <input type="hidden" name="searchPaths.config" value="{}">
  460. </div>
  461. <div class="path-cache">
  462. <input type="checkbox" name="searchPaths.cache" data-dtype="Boolean" checked/>
  463. </div>
  464. <div class="path-controls">
  465. <a class="delete-path" title="Delete path"><i class="fas fa-trash"></i></a>
  466. </div>
  467. </li>
  468. `;
  469. table.append(row);
  470. this._reIndexPaths(table);
  471. this.setPosition(); // Auto-resize window
  472. }
  473. async _reIndexPaths(table) {
  474. table
  475. .find('.path-source')
  476. .find('input')
  477. .each(function (index) {
  478. $(this).attr('name', `searchPaths.${index}.source`);
  479. });
  480. table
  481. .find('.path-text')
  482. .find('input')
  483. .each(function (index) {
  484. $(this).attr('name', `searchPaths.${index}.text`);
  485. });
  486. table
  487. .find('.path-cache')
  488. .find('input')
  489. .each(function (index) {
  490. $(this).attr('name', `searchPaths.${index}.cache`);
  491. });
  492. table
  493. .find('.path-category')
  494. .find('input')
  495. .each(function (index) {
  496. $(this).attr('name', `searchPaths.${index}.types`);
  497. });
  498. table
  499. .find('.path-config')
  500. .find('input')
  501. .each(function (index) {
  502. $(this).attr('name', `searchPaths.${index}.config`);
  503. });
  504. }
  505. async _onDeletePath(event) {
  506. event.preventDefault();
  507. const li = event.currentTarget.closest('.table-row');
  508. li.remove();
  509. const table = $(event.currentTarget).closest('.token-variant-table');
  510. this._reIndexPaths(table);
  511. this.setPosition(); // Auto-resize window
  512. }
  513. async _onSearchSourceTextChange(event) {
  514. const image = this._pathIcon(event.target.value);
  515. const imgur = image === 'fas fa-info';
  516. const json = image === 'fas fa-brackets-curly';
  517. const imgurControl = $(event.currentTarget).closest('.table-row').find('.imgur-control');
  518. if (imgur) imgurControl.addClass('active');
  519. else imgurControl.removeClass('active');
  520. const jsonControl = $(event.currentTarget).closest('.table-row').find('.json-control');
  521. if (json) jsonControl.addClass('active');
  522. else jsonControl.removeClass('active');
  523. $(event.currentTarget).closest('.table-row').find('.path-image i').attr('class', image);
  524. }
  525. // Return icon appropriate for the path provided
  526. _pathIcon(source) {
  527. if (source.startsWith('s3')) {
  528. return 'fas fa-database';
  529. } else if (source.startsWith('rolltable')) {
  530. return 'fas fa-dice';
  531. } else if (source.startsWith('forgevtt') || source.startsWith('forge-bazaar')) {
  532. return 'fas fa-hammer';
  533. } else if (source.startsWith('imgur')) {
  534. return 'fas fa-info';
  535. } else if (source.startsWith('json')) {
  536. return 'fas fa-brackets-curly';
  537. }
  538. return 'fas fa-folder';
  539. }
  540. /**
  541. * @param {Event} event
  542. * @param {Object} formData
  543. */
  544. async _updateObject(event, formData) {
  545. const settings = this.settings;
  546. formData = expandObject(formData);
  547. // Search Paths
  548. settings.searchPaths = formData.hasOwnProperty('searchPaths')
  549. ? Object.values(formData.searchPaths)
  550. : [];
  551. settings.searchPaths.forEach((path) => {
  552. if (!path.source) path.source = 'data';
  553. if (path.types) path.types = path.types.split(',');
  554. else path.types = [];
  555. if (path.config) {
  556. try {
  557. path.config = JSON.parse(path.config);
  558. } catch (e) {
  559. delete path.config;
  560. }
  561. } else delete path.config;
  562. });
  563. // Search Filters
  564. for (const filter in formData.searchFilters) {
  565. if (!this._validRegex(formData.searchFilters[filter].regex))
  566. formData.searchFilters[filter].regex = '';
  567. }
  568. mergeObject(settings.searchFilters, formData.searchFilters);
  569. // Algorithm
  570. formData.algorithm.fuzzyLimit = parseInt(formData.algorithm.fuzzyLimit);
  571. if (isNaN(formData.algorithm.fuzzyLimit) || formData.algorithm.fuzzyLimit < 1)
  572. formData.algorithm.fuzzyLimit = 50;
  573. formData.algorithm.fuzzyThreshold = (100 - formData.algorithm.fuzzyThreshold) / 100;
  574. mergeObject(settings.algorithm, formData.algorithm);
  575. // Randomizer
  576. mergeObject(settings.randomizer, formData.randomizer);
  577. // Pop-up
  578. mergeObject(settings.popup, formData.popup);
  579. // Permissions
  580. mergeObject(settings.permissions, formData.permissions);
  581. // Token HUD
  582. mergeObject(settings.worldHud, formData.worldHud);
  583. // Internal Effects
  584. mergeObject(settings.internalEffects, formData.internalEffects);
  585. // Misc
  586. mergeObject(settings, {
  587. keywordSearch: formData.keywordSearch,
  588. excludedKeywords: formData.excludedKeywords,
  589. systemHpPath: formData.systemHpPath?.trim(),
  590. runSearchOnPath: formData.runSearchOnPath,
  591. imgurClientId: formData.imgurClientId,
  592. enableStatusConfig: formData.enableStatusConfig,
  593. disableNotifs: formData.disableNotifs,
  594. staticCache: formData.staticCache,
  595. staticCacheFile: formData.staticCacheFile,
  596. tilesEnabled: formData.tilesEnabled,
  597. stackStatusConfig: formData.stackStatusConfig,
  598. mergeGroup: formData.mergeGroup,
  599. customImageCategories: (formData.customImageCategories || '')
  600. .split(',')
  601. .map((t) => t.trim())
  602. .filter((t) => t),
  603. disableEffectIcons: formData.disableEffectIcons,
  604. displayEffectIconsOnHover: formData.displayEffectIconsOnHover,
  605. filterEffectIcons: formData.filterEffectIcons,
  606. hideElevationTooltip: formData.hideElevationTooltip,
  607. hideTokenBorder: formData.hideTokenBorder,
  608. filterCustomEffectIcons: formData.filterCustomEffectIcons,
  609. filterIconList: (formData.filterIconList || '')
  610. .split(',')
  611. .map((t) => t.trim())
  612. .filter((t) => t),
  613. updateTokenProto: formData.updateTokenProto,
  614. imgNameContainsDimensions: formData.imgNameContainsDimensions,
  615. imgNameContainsFADimensions: formData.imgNameContainsFADimensions,
  616. playVideoOnHover: formData.playVideoOnHover,
  617. pauseVideoOnHoverOut: formData.pauseVideoOnHoverOut,
  618. disableImageChangeOnPolymorphed: formData.disableImageChangeOnPolymorphed,
  619. disableImageUpdateOnNonPrototype: formData.disableImageUpdateOnNonPrototype,
  620. disableTokenUpdateAnimation: formData.disableTokenUpdateAnimation,
  621. mappingsCurrentSceneOnly: formData.mappingsCurrentSceneOnly,
  622. });
  623. // Global Mappings
  624. settings.globalMappings = TVA_CONFIG.globalMappings;
  625. // Save Settings
  626. if (this.dummySettings) {
  627. mergeObjectFix(this.dummySettings, settings, { insertKeys: false });
  628. } else {
  629. updateSettings(settings);
  630. }
  631. }
  632. }
  633. // ========================
  634. // v8 support, broken merge
  635. // ========================
  636. export function mergeObjectFix(
  637. original,
  638. other = {},
  639. {
  640. insertKeys = true,
  641. insertValues = true,
  642. overwrite = true,
  643. recursive = true,
  644. inplace = true,
  645. enforceTypes = false,
  646. } = {},
  647. _d = 0
  648. ) {
  649. other = other || {};
  650. if (!(original instanceof Object) || !(other instanceof Object)) {
  651. throw new Error('One of original or other are not Objects!');
  652. }
  653. const options = { insertKeys, insertValues, overwrite, recursive, inplace, enforceTypes };
  654. // Special handling at depth 0
  655. if (_d === 0) {
  656. if (!inplace) original = deepClone(original);
  657. if (Object.keys(original).some((k) => /\./.test(k))) original = expandObject(original);
  658. if (Object.keys(other).some((k) => /\./.test(k))) other = expandObject(other);
  659. }
  660. // Iterate over the other object
  661. for (let k of Object.keys(other)) {
  662. const v = other[k];
  663. if (original.hasOwnProperty(k)) _mergeUpdate(original, k, v, options, _d + 1);
  664. else _mergeInsertFix(original, k, v, options, _d + 1);
  665. }
  666. return original;
  667. }
  668. function _mergeInsertFix(original, k, v, { insertKeys, insertValues } = {}, _d) {
  669. // Recursively create simple objects
  670. if (v?.constructor === Object && insertKeys) {
  671. original[k] = mergeObjectFix({}, v, { insertKeys: true, inplace: true });
  672. return;
  673. }
  674. // Delete a key
  675. if (k.startsWith('-=')) {
  676. delete original[k.slice(2)];
  677. return;
  678. }
  679. // Insert a key
  680. const canInsert = (_d <= 1 && insertKeys) || (_d > 1 && insertValues);
  681. if (canInsert) original[k] = v;
  682. }
  683. function _mergeUpdate(
  684. original,
  685. k,
  686. v,
  687. { insertKeys, insertValues, enforceTypes, overwrite, recursive } = {},
  688. _d
  689. ) {
  690. const x = original[k];
  691. const tv = getType(v);
  692. const tx = getType(x);
  693. // Recursively merge an inner object
  694. if (tv === 'Object' && tx === 'Object' && recursive) {
  695. return mergeObjectFix(
  696. x,
  697. v,
  698. {
  699. insertKeys: insertKeys,
  700. insertValues: insertValues,
  701. overwrite: overwrite,
  702. inplace: true,
  703. enforceTypes: enforceTypes,
  704. },
  705. _d
  706. );
  707. }
  708. // Overwrite an existing value
  709. if (overwrite) {
  710. if (tx !== 'undefined' && tv !== tx && enforceTypes) {
  711. throw new Error(`Mismatched data types encountered during object merge.`);
  712. }
  713. original[k] = v;
  714. }
  715. }