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.

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