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.

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