import { cacheImages } from '../scripts/search.js';
|
|
import { TVA_CONFIG, updateSettings } from '../scripts/settings.js';
|
|
import { getFileName } from '../scripts/utils.js';
|
|
import EffectMappingForm from './effectMappingForm.js';
|
|
import { showPathSelectCategoryDialog, showPathSelectConfigForm } from './dialogs.js';
|
|
|
|
export default class ConfigureSettings extends FormApplication {
|
|
constructor(
|
|
dummySettings,
|
|
{
|
|
searchPaths = true,
|
|
searchFilters = true,
|
|
searchAlgorithm = true,
|
|
randomizer = true,
|
|
popup = true,
|
|
permissions = true,
|
|
worldHud = true,
|
|
misc = true,
|
|
activeEffects = true,
|
|
features = false,
|
|
} = {}
|
|
) {
|
|
super({}, {});
|
|
this.enabledTabs = {
|
|
searchPaths,
|
|
searchFilters,
|
|
searchAlgorithm,
|
|
randomizer,
|
|
features,
|
|
popup,
|
|
permissions,
|
|
worldHud,
|
|
misc,
|
|
activeEffects,
|
|
};
|
|
this.settings = foundry.utils.deepClone(TVA_CONFIG);
|
|
if (dummySettings) {
|
|
this.settings = mergeObject(this.settings, dummySettings, { insertKeys: false });
|
|
this.dummySettings = dummySettings;
|
|
}
|
|
}
|
|
|
|
static get defaultOptions() {
|
|
return mergeObject(super.defaultOptions, {
|
|
id: 'token-variants-configure-settings',
|
|
classes: ['sheet'],
|
|
template: 'modules/token-variants/templates/configureSettings.html',
|
|
resizable: false,
|
|
minimizable: false,
|
|
title: 'Configure Settings',
|
|
width: 700,
|
|
height: 'auto',
|
|
tabs: [{ navSelector: '.sheet-tabs', contentSelector: '.content', initial: 'searchPaths' }],
|
|
});
|
|
}
|
|
|
|
async getData(options) {
|
|
const data = super.getData(options);
|
|
const settings = this.settings;
|
|
|
|
data.enabledTabs = this.enabledTabs;
|
|
|
|
// === Search Paths ===
|
|
const paths = settings.searchPaths.map((path) => {
|
|
const r = {};
|
|
r.text = path.text;
|
|
r.icon = this._pathIcon(path.source || '');
|
|
r.cache = path.cache;
|
|
r.source = path.source || '';
|
|
r.types = path.types.join(',');
|
|
r.config = JSON.stringify(path.config ?? {});
|
|
r.hasConfig = path.config && !isEmpty(path.config);
|
|
return r;
|
|
});
|
|
data.searchPaths = paths;
|
|
|
|
// === Search Filters ===
|
|
data.searchFilters = settings.searchFilters;
|
|
for (const filter in data.searchFilters) {
|
|
data.searchFilters[filter].label = filter;
|
|
}
|
|
|
|
// === Algorithm ===
|
|
data.algorithm = deepClone(settings.algorithm);
|
|
data.algorithm.fuzzyThreshold = 100 - data.algorithm.fuzzyThreshold * 100;
|
|
|
|
// === Randomizer ===
|
|
// Get all actor types defined by the game system
|
|
data.randomizer = deepClone(settings.randomizer);
|
|
const actorTypes = (game.system.entityTypes ?? game.system.documentTypes)['Actor'];
|
|
data.randomizer.actorTypes = actorTypes.reduce((obj, t) => {
|
|
const label = CONFIG['Actor']?.typeLabels?.[t] ?? t;
|
|
obj[t] = {
|
|
label: game.i18n.has(label) ? game.i18n.localize(label) : t,
|
|
disable: settings.randomizer[`${t}Disable`] ?? false,
|
|
};
|
|
return obj;
|
|
}, {});
|
|
|
|
data.randomizer.tokenToPortraitDisabled =
|
|
!(settings.randomizer.tokenCreate || settings.randomizer.tokenCopyPaste) ||
|
|
data.randomizer.diffImages;
|
|
|
|
// === Pop-up ===
|
|
data.popup = deepClone(settings.popup);
|
|
// Get all actor types defined by the game system
|
|
data.popup.actorTypes = actorTypes.reduce((obj, t) => {
|
|
const label = CONFIG['Actor']?.typeLabels?.[t] ?? t;
|
|
obj[t] = {
|
|
type: t,
|
|
label: game.i18n.has(label) ? game.i18n.localize(label) : t,
|
|
disable: settings.popup[`${t}Disable`] ?? false,
|
|
};
|
|
return obj;
|
|
}, {});
|
|
|
|
// Split into arrays of max length 3
|
|
let allTypes = [];
|
|
let tempTypes = [];
|
|
let i = 0;
|
|
for (const [key, value] of Object.entries(data.popup.actorTypes)) {
|
|
tempTypes.push(value);
|
|
i++;
|
|
if (i % 3 == 0) {
|
|
allTypes.push(tempTypes);
|
|
tempTypes = [];
|
|
}
|
|
}
|
|
if (tempTypes.length > 0) allTypes.push(tempTypes);
|
|
data.popup.actorTypes = allTypes;
|
|
|
|
// === Permissions ===
|
|
data.permissions = settings.permissions;
|
|
|
|
// === Token HUD ===
|
|
data.worldHud = deepClone(settings.worldHud);
|
|
data.worldHud.tokenHUDWildcardActive = game.modules.get('token-hud-wildcard')?.active;
|
|
|
|
// === Internal Effects ===
|
|
data.internalEffects = deepClone(settings.internalEffects);
|
|
|
|
// === Misc ===
|
|
data.keywordSearch = settings.keywordSearch;
|
|
data.excludedKeywords = settings.excludedKeywords;
|
|
data.systemHpPath = settings.systemHpPath;
|
|
data.runSearchOnPath = settings.runSearchOnPath;
|
|
data.imgurClientId = settings.imgurClientId;
|
|
data.enableStatusConfig = settings.enableStatusConfig;
|
|
data.disableNotifs = settings.disableNotifs;
|
|
data.staticCache = settings.staticCache;
|
|
data.staticCacheFile = settings.staticCacheFile;
|
|
data.stackStatusConfig = settings.stackStatusConfig;
|
|
data.mergeGroup = settings.mergeGroup;
|
|
data.customImageCategories = settings.customImageCategories.join(',');
|
|
data.disableEffectIcons = settings.disableEffectIcons;
|
|
data.displayEffectIconsOnHover = settings.displayEffectIconsOnHover;
|
|
data.filterEffectIcons = settings.filterEffectIcons;
|
|
data.hideElevationTooltip = settings.hideElevationTooltip;
|
|
data.hideTokenBorder = settings.hideTokenBorder;
|
|
data.filterCustomEffectIcons = settings.filterCustomEffectIcons;
|
|
data.filterIconList = settings.filterIconList.join(',');
|
|
data.tilesEnabled = settings.tilesEnabled;
|
|
data.updateTokenProto = settings.updateTokenProto;
|
|
data.imgNameContainsDimensions = settings.imgNameContainsDimensions;
|
|
data.imgNameContainsFADimensions = settings.imgNameContainsFADimensions;
|
|
data.playVideoOnHover = settings.playVideoOnHover;
|
|
data.pauseVideoOnHoverOut = settings.pauseVideoOnHoverOut;
|
|
data.disableImageChangeOnPolymorphed = settings.disableImageChangeOnPolymorphed;
|
|
data.disableImageUpdateOnNonPrototype = settings.disableImageUpdateOnNonPrototype;
|
|
data.disableTokenUpdateAnimation = settings.disableTokenUpdateAnimation;
|
|
data.mappingsCurrentSceneOnly = settings.mappingsCurrentSceneOnly;
|
|
|
|
// Controls
|
|
data.pathfinder = ['pf1e', 'pf2e'].includes(game.system.id);
|
|
data.dnd5e = game.system.id === 'dnd5e';
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* @param {JQuery} html
|
|
*/
|
|
activateListeners(html) {
|
|
super.activateListeners(html);
|
|
|
|
// Search Paths
|
|
super.activateListeners(html);
|
|
html.find('a.create-path').click(this._onCreatePath.bind(this));
|
|
html.on('input', '.searchSource', this._onSearchSourceTextChange.bind(this));
|
|
$(html).on('click', 'a.delete-path', this._onDeletePath.bind(this));
|
|
$(html).on('click', 'a.convert-imgur', this._onConvertImgurPath.bind(this));
|
|
$(html).on('click', 'a.convert-json', this._onConvertJsonPath.bind(this));
|
|
$(html).on('click', '.path-image.source-icon a', this._onBrowseFolder.bind(this));
|
|
$(html).on('click', 'a.select-category', showPathSelectCategoryDialog.bind(this));
|
|
$(html).on('click', 'a.select-config', showPathSelectConfigForm.bind(this));
|
|
|
|
// Search Filters
|
|
html.on('input', 'input.filterRegex', this._validateRegex.bind(this));
|
|
|
|
// Active Effects
|
|
const disableEffectIcons = html.find('[name="disableEffectIcons"]');
|
|
const filterEffectIcons = html.find('[name="filterEffectIcons"]');
|
|
disableEffectIcons
|
|
.on('change', (e) => {
|
|
if (e.target.checked) filterEffectIcons.prop('checked', false);
|
|
})
|
|
.trigger('change');
|
|
filterEffectIcons.on('change', (e) => {
|
|
if (e.target.checked) disableEffectIcons.prop('checked', false);
|
|
});
|
|
|
|
// Algorithm
|
|
const algorithmTab = $(html).find('div[data-tab="searchAlgorithm"]');
|
|
algorithmTab.find(`input[name="algorithm.exact"]`).change((e) => {
|
|
$(e.target)
|
|
.closest('form')
|
|
.find('input[name="algorithm.fuzzy"]')
|
|
.prop('checked', !e.target.checked);
|
|
});
|
|
algorithmTab.find(`input[name="algorithm.fuzzy"]`).change((e) => {
|
|
$(e.target)
|
|
.closest('form')
|
|
.find('input[name="algorithm.exact"]')
|
|
.prop('checked', !e.target.checked);
|
|
});
|
|
algorithmTab.find('input[name="algorithm.fuzzyThreshold"]').change((e) => {
|
|
$(e.target).siblings('.token-variants-range-value').html(`${e.target.value}%`);
|
|
});
|
|
|
|
// Randomizer
|
|
const tokenCreate = html.find('input[name="randomizer.tokenCreate"]');
|
|
const tokenCopyPaste = html.find('input[name="randomizer.tokenCopyPaste"]');
|
|
const tokenToPortrait = html.find('input[name="randomizer.tokenToPortrait"]');
|
|
const _toggle = () => {
|
|
tokenToPortrait.prop(
|
|
'disabled',
|
|
!(tokenCreate.is(':checked') || tokenCopyPaste.is(':checked'))
|
|
);
|
|
};
|
|
tokenCreate.change(_toggle);
|
|
tokenCopyPaste.change(_toggle);
|
|
|
|
const diffImages = html.find('input[name="randomizer.diffImages"]');
|
|
const syncImages = html.find('input[name="randomizer.syncImages"]');
|
|
diffImages.change(() => {
|
|
syncImages.prop('disabled', !diffImages.is(':checked'));
|
|
tokenToPortrait.prop('disabled', diffImages.is(':checked'));
|
|
});
|
|
|
|
// Token HUD
|
|
html.find('input[name="worldHud.updateActorImage"]').change((event) => {
|
|
$(event.target)
|
|
.closest('form')
|
|
.find('input[name="worldHud.useNameSimilarity"]')
|
|
.prop('disabled', !event.target.checked);
|
|
});
|
|
|
|
// Static Cache
|
|
html.find('button.token-variants-cache-images').click((event) => {
|
|
const tab = $(event.target).closest('.tab');
|
|
const staticOn = tab.find('input[name="staticCache"]');
|
|
const staticFile = tab.find('input[name="staticCacheFile"]');
|
|
cacheImages({ staticCache: staticOn.is(':checked'), staticCacheFile: staticFile.val() });
|
|
});
|
|
|
|
// Global Mappings
|
|
html.find('button.token-variants-global-mapping').click(() => {
|
|
const setting = game.settings.get('core', DefaultTokenConfig.SETTING);
|
|
const data = new foundry.data.PrototypeToken(setting);
|
|
const token = new TokenDocument(data, { actor: null });
|
|
new EffectMappingForm(token, { globalMappings: true }).render(true);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validates regex entered into Search Filter's RegEx input field
|
|
*/
|
|
async _validateRegex(event) {
|
|
if (this._validRegex(event.target.value)) {
|
|
event.target.style.backgroundColor = '';
|
|
} else {
|
|
event.target.style.backgroundColor = '#ff7066';
|
|
}
|
|
}
|
|
|
|
_validRegex(val) {
|
|
if (val) {
|
|
try {
|
|
new RegExp(val);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Open a FilePicker so the user can select a local folder to use as an image source
|
|
*/
|
|
async _onBrowseFolder(event) {
|
|
const pathInput = $(event.target).closest('.table-row').find('.path-text input');
|
|
const sourceInput = $(event.target).closest('.table-row').find('.path-source input');
|
|
|
|
let activeSource = sourceInput.val() || 'data';
|
|
let current = pathInput.val();
|
|
|
|
if (activeSource.startsWith('s3:')) {
|
|
const bucketName = activeSource.replace('s3:', '');
|
|
current = `${game.data.files.s3?.endpoint.protocol}//${bucketName}.${game.data.files.s3?.endpoint.host}/${current}`;
|
|
} else if (activeSource.startsWith('rolltable')) {
|
|
let content = `<select style="width: 100%;" name="table-name" id="output-tableKey">`;
|
|
|
|
game.tables.forEach((rollTable) => {
|
|
content += `<option value='${rollTable.name}'>${rollTable.name}</option>`;
|
|
});
|
|
|
|
content += `</select>`;
|
|
|
|
new Dialog({
|
|
title: `Select a Rolltable`,
|
|
content: content,
|
|
buttons: {
|
|
yes: {
|
|
icon: "<i class='fas fa-check'></i>",
|
|
label: 'Select',
|
|
callback: (html) => {
|
|
pathInput.val();
|
|
const tableName = html.find("select[name='table-name']").val();
|
|
pathInput.val(tableName);
|
|
},
|
|
},
|
|
},
|
|
default: 'yes',
|
|
}).render(true);
|
|
return;
|
|
}
|
|
|
|
if (activeSource === 'json') {
|
|
new FilePicker({
|
|
type: 'text',
|
|
activeSource: 'data',
|
|
current: current,
|
|
callback: (path, fp) => {
|
|
pathInput.val(path);
|
|
},
|
|
}).render(true);
|
|
} else {
|
|
new FilePicker({
|
|
type: 'folder',
|
|
activeSource: activeSource,
|
|
current: current,
|
|
callback: (path, fp) => {
|
|
pathInput.val(fp.result.target);
|
|
if (fp.activeSource === 's3') {
|
|
sourceInput.val(`s3:${fp.result.bucket}`);
|
|
} else {
|
|
sourceInput.val(fp.activeSource);
|
|
}
|
|
},
|
|
}).render(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts Imgur path to a rolltable
|
|
*/
|
|
async _onConvertImgurPath(event) {
|
|
event.preventDefault();
|
|
|
|
const pathInput = $(event.target).closest('.table-row').find('.path-text input');
|
|
const sourceInput = $(event.target).closest('.table-row').find('.path-source input');
|
|
|
|
const albumHash = pathInput.val();
|
|
const imgurClientId =
|
|
TVA_CONFIG.imgurClientId === '' ? 'df9d991443bb222' : TVA_CONFIG.imgurClientId;
|
|
|
|
fetch('https://api.imgur.com/3/gallery/album/' + albumHash, {
|
|
headers: {
|
|
Authorization: 'Client-ID ' + imgurClientId,
|
|
Accept: 'application/json',
|
|
},
|
|
})
|
|
.then((response) => response.json())
|
|
.then(
|
|
async function (result) {
|
|
if (!result.success && location.hostname === 'localhost') {
|
|
ui.notifications.warn(
|
|
game.i18n.format('token-variants.notifications.warn.imgur-localhost')
|
|
);
|
|
return;
|
|
}
|
|
|
|
const data = result.data;
|
|
|
|
let resultsArray = [];
|
|
data.images.forEach((img, i) => {
|
|
resultsArray.push({
|
|
type: 0,
|
|
text: img.title ?? img.description ?? '',
|
|
weight: 1,
|
|
range: [i + 1, i + 1],
|
|
collection: 'Text',
|
|
drawn: false,
|
|
img: img.link,
|
|
});
|
|
});
|
|
|
|
await RollTable.create({
|
|
name: data.title,
|
|
description:
|
|
'Token Variant Art auto generated RollTable: https://imgur.com/gallery/' + albumHash,
|
|
results: resultsArray,
|
|
replacement: true,
|
|
displayRoll: true,
|
|
img: 'modules/token-variants/img/token-images.svg',
|
|
});
|
|
|
|
pathInput.val(data.title);
|
|
sourceInput.val('rolltable').trigger('input');
|
|
}.bind(this)
|
|
)
|
|
.catch((error) => console.warn('TVA | ', error));
|
|
}
|
|
|
|
/**
|
|
* Converts Json path to a rolltable
|
|
*/
|
|
async _onConvertJsonPath(event) {
|
|
event.preventDefault();
|
|
|
|
const pathInput = $(event.target).closest('.table-row').find('.path-text input');
|
|
const sourceInput = $(event.target).closest('.table-row').find('.path-source input');
|
|
|
|
const jsonPath = pathInput.val();
|
|
|
|
fetch(jsonPath, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
})
|
|
.then((response) => response.json())
|
|
.then(
|
|
async function (result) {
|
|
if (!result.length > 0) {
|
|
ui.notifications.warn(
|
|
game.i18n.format('token-variants.notifications.warn.json-localhost')
|
|
);
|
|
return;
|
|
}
|
|
|
|
const data = result;
|
|
data.title = getFileName(jsonPath);
|
|
|
|
let resultsArray = [];
|
|
data.forEach((img, i) => {
|
|
resultsArray.push({
|
|
type: 0,
|
|
text: img.name ?? '',
|
|
weight: 1,
|
|
range: [i + 1, i + 1],
|
|
collection: 'Text',
|
|
drawn: false,
|
|
img: img.path,
|
|
});
|
|
});
|
|
|
|
await RollTable.create({
|
|
name: data.title,
|
|
description: 'Token Variant Art auto generated RollTable: ' + jsonPath,
|
|
results: resultsArray,
|
|
replacement: true,
|
|
displayRoll: true,
|
|
img: 'modules/token-variants/img/token-images.svg',
|
|
});
|
|
|
|
pathInput.val(data.title);
|
|
sourceInput.val('rolltable').trigger('input');
|
|
}.bind(this)
|
|
)
|
|
.catch((error) => console.warn('TVA | ', error));
|
|
}
|
|
|
|
/**
|
|
* Generates a new search path row
|
|
*/
|
|
async _onCreatePath(event) {
|
|
event.preventDefault();
|
|
const table = $(event.currentTarget).closest('.token-variant-table');
|
|
let row = `
|
|
<li class="table-row flexrow">
|
|
<div class="path-image source-icon">
|
|
<a><i class="${this._pathIcon('')}"></i></a>
|
|
</div>
|
|
<div class="path-source">
|
|
<input class="searchSource" type="text" name="searchPaths.source" value="" placeholder="data"/>
|
|
</div>
|
|
<div class="path-text">
|
|
<input class="searchPath" type="text" name="searchPaths.text" value="" placeholder="Path to folder"/>
|
|
</div>
|
|
<div class="imgur-control">
|
|
<a class="convert-imgur" title="Convert to Rolltable"><i class="fas fa-angle-double-left"></i></a>
|
|
</div>
|
|
<div class="json-control">
|
|
<a class="convert-json" title="Convert to Rolltable"><i class="fas fa-angle-double-left"></i></a>
|
|
</div>
|
|
<div class="path-category">
|
|
<a class="select-category" title="Select image categories/filters"><i class="fas fa-swatchbook"></i></a>
|
|
<input type="hidden" name="searchPaths.types" value="Portrait,Token,PortraitAndToken">
|
|
</div>
|
|
<div class="path-config">
|
|
<a class="select-config" title="Apply configuration to images under this path."><i class="fas fa-cog fa-lg"></i></a>
|
|
<input type="hidden" name="searchPaths.config" value="{}">
|
|
</div>
|
|
<div class="path-cache">
|
|
<input type="checkbox" name="searchPaths.cache" data-dtype="Boolean" checked/>
|
|
</div>
|
|
<div class="path-controls">
|
|
<a class="delete-path" title="Delete path"><i class="fas fa-trash"></i></a>
|
|
</div>
|
|
</li>
|
|
`;
|
|
table.append(row);
|
|
|
|
this._reIndexPaths(table);
|
|
|
|
this.setPosition(); // Auto-resize window
|
|
}
|
|
|
|
async _reIndexPaths(table) {
|
|
table
|
|
.find('.path-source')
|
|
.find('input')
|
|
.each(function (index) {
|
|
$(this).attr('name', `searchPaths.${index}.source`);
|
|
});
|
|
|
|
table
|
|
.find('.path-text')
|
|
.find('input')
|
|
.each(function (index) {
|
|
$(this).attr('name', `searchPaths.${index}.text`);
|
|
});
|
|
|
|
table
|
|
.find('.path-cache')
|
|
.find('input')
|
|
.each(function (index) {
|
|
$(this).attr('name', `searchPaths.${index}.cache`);
|
|
});
|
|
table
|
|
.find('.path-category')
|
|
.find('input')
|
|
.each(function (index) {
|
|
$(this).attr('name', `searchPaths.${index}.types`);
|
|
});
|
|
table
|
|
.find('.path-config')
|
|
.find('input')
|
|
.each(function (index) {
|
|
$(this).attr('name', `searchPaths.${index}.config`);
|
|
});
|
|
}
|
|
|
|
async _onDeletePath(event) {
|
|
event.preventDefault();
|
|
|
|
const li = event.currentTarget.closest('.table-row');
|
|
li.remove();
|
|
|
|
const table = $(event.currentTarget).closest('.token-variant-table');
|
|
this._reIndexPaths(table);
|
|
|
|
this.setPosition(); // Auto-resize window
|
|
}
|
|
|
|
async _onSearchSourceTextChange(event) {
|
|
const image = this._pathIcon(event.target.value);
|
|
const imgur = image === 'fas fa-info';
|
|
const json = image === 'fas fa-brackets-curly';
|
|
|
|
const imgurControl = $(event.currentTarget).closest('.table-row').find('.imgur-control');
|
|
if (imgur) imgurControl.addClass('active');
|
|
else imgurControl.removeClass('active');
|
|
|
|
const jsonControl = $(event.currentTarget).closest('.table-row').find('.json-control');
|
|
if (json) jsonControl.addClass('active');
|
|
else jsonControl.removeClass('active');
|
|
|
|
$(event.currentTarget).closest('.table-row').find('.path-image i').attr('class', image);
|
|
}
|
|
|
|
// Return icon appropriate for the path provided
|
|
_pathIcon(source) {
|
|
if (source.startsWith('s3')) {
|
|
return 'fas fa-database';
|
|
} else if (source.startsWith('rolltable')) {
|
|
return 'fas fa-dice';
|
|
} else if (source.startsWith('forgevtt') || source.startsWith('forge-bazaar')) {
|
|
return 'fas fa-hammer';
|
|
} else if (source.startsWith('imgur')) {
|
|
return 'fas fa-info';
|
|
} else if (source.startsWith('json')) {
|
|
return 'fas fa-brackets-curly';
|
|
}
|
|
|
|
return 'fas fa-folder';
|
|
}
|
|
|
|
/**
|
|
* @param {Event} event
|
|
* @param {Object} formData
|
|
*/
|
|
async _updateObject(event, formData) {
|
|
const settings = this.settings;
|
|
formData = expandObject(formData);
|
|
|
|
// Search Paths
|
|
settings.searchPaths = formData.hasOwnProperty('searchPaths')
|
|
? Object.values(formData.searchPaths)
|
|
: [];
|
|
settings.searchPaths.forEach((path) => {
|
|
if (!path.source) path.source = 'data';
|
|
if (path.types) path.types = path.types.split(',');
|
|
else path.types = [];
|
|
if (path.config) {
|
|
try {
|
|
path.config = JSON.parse(path.config);
|
|
} catch (e) {
|
|
delete path.config;
|
|
}
|
|
} else delete path.config;
|
|
});
|
|
|
|
// Search Filters
|
|
for (const filter in formData.searchFilters) {
|
|
if (!this._validRegex(formData.searchFilters[filter].regex))
|
|
formData.searchFilters[filter].regex = '';
|
|
}
|
|
mergeObject(settings.searchFilters, formData.searchFilters);
|
|
|
|
// Algorithm
|
|
formData.algorithm.fuzzyLimit = parseInt(formData.algorithm.fuzzyLimit);
|
|
if (isNaN(formData.algorithm.fuzzyLimit) || formData.algorithm.fuzzyLimit < 1)
|
|
formData.algorithm.fuzzyLimit = 50;
|
|
formData.algorithm.fuzzyThreshold = (100 - formData.algorithm.fuzzyThreshold) / 100;
|
|
mergeObject(settings.algorithm, formData.algorithm);
|
|
|
|
// Randomizer
|
|
mergeObject(settings.randomizer, formData.randomizer);
|
|
|
|
// Pop-up
|
|
mergeObject(settings.popup, formData.popup);
|
|
|
|
// Permissions
|
|
mergeObject(settings.permissions, formData.permissions);
|
|
|
|
// Token HUD
|
|
mergeObject(settings.worldHud, formData.worldHud);
|
|
|
|
// Internal Effects
|
|
mergeObject(settings.internalEffects, formData.internalEffects);
|
|
|
|
// Misc
|
|
mergeObject(settings, {
|
|
keywordSearch: formData.keywordSearch,
|
|
excludedKeywords: formData.excludedKeywords,
|
|
systemHpPath: formData.systemHpPath?.trim(),
|
|
runSearchOnPath: formData.runSearchOnPath,
|
|
imgurClientId: formData.imgurClientId,
|
|
enableStatusConfig: formData.enableStatusConfig,
|
|
disableNotifs: formData.disableNotifs,
|
|
staticCache: formData.staticCache,
|
|
staticCacheFile: formData.staticCacheFile,
|
|
tilesEnabled: formData.tilesEnabled,
|
|
stackStatusConfig: formData.stackStatusConfig,
|
|
mergeGroup: formData.mergeGroup,
|
|
customImageCategories: (formData.customImageCategories || '')
|
|
.split(',')
|
|
.map((t) => t.trim())
|
|
.filter((t) => t),
|
|
disableEffectIcons: formData.disableEffectIcons,
|
|
displayEffectIconsOnHover: formData.displayEffectIconsOnHover,
|
|
filterEffectIcons: formData.filterEffectIcons,
|
|
hideElevationTooltip: formData.hideElevationTooltip,
|
|
hideTokenBorder: formData.hideTokenBorder,
|
|
filterCustomEffectIcons: formData.filterCustomEffectIcons,
|
|
filterIconList: (formData.filterIconList || '')
|
|
.split(',')
|
|
.map((t) => t.trim())
|
|
.filter((t) => t),
|
|
updateTokenProto: formData.updateTokenProto,
|
|
imgNameContainsDimensions: formData.imgNameContainsDimensions,
|
|
imgNameContainsFADimensions: formData.imgNameContainsFADimensions,
|
|
playVideoOnHover: formData.playVideoOnHover,
|
|
pauseVideoOnHoverOut: formData.pauseVideoOnHoverOut,
|
|
disableImageChangeOnPolymorphed: formData.disableImageChangeOnPolymorphed,
|
|
disableImageUpdateOnNonPrototype: formData.disableImageUpdateOnNonPrototype,
|
|
disableTokenUpdateAnimation: formData.disableTokenUpdateAnimation,
|
|
mappingsCurrentSceneOnly: formData.mappingsCurrentSceneOnly,
|
|
});
|
|
|
|
// Global Mappings
|
|
settings.globalMappings = TVA_CONFIG.globalMappings;
|
|
|
|
// Save Settings
|
|
if (this.dummySettings) {
|
|
mergeObjectFix(this.dummySettings, settings, { insertKeys: false });
|
|
} else {
|
|
updateSettings(settings);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================
|
|
// v8 support, broken merge
|
|
// ========================
|
|
export function mergeObjectFix(
|
|
original,
|
|
other = {},
|
|
{
|
|
insertKeys = true,
|
|
insertValues = true,
|
|
overwrite = true,
|
|
recursive = true,
|
|
inplace = true,
|
|
enforceTypes = false,
|
|
} = {},
|
|
_d = 0
|
|
) {
|
|
other = other || {};
|
|
if (!(original instanceof Object) || !(other instanceof Object)) {
|
|
throw new Error('One of original or other are not Objects!');
|
|
}
|
|
const options = { insertKeys, insertValues, overwrite, recursive, inplace, enforceTypes };
|
|
|
|
// Special handling at depth 0
|
|
if (_d === 0) {
|
|
if (!inplace) original = deepClone(original);
|
|
if (Object.keys(original).some((k) => /\./.test(k))) original = expandObject(original);
|
|
if (Object.keys(other).some((k) => /\./.test(k))) other = expandObject(other);
|
|
}
|
|
|
|
// Iterate over the other object
|
|
for (let k of Object.keys(other)) {
|
|
const v = other[k];
|
|
if (original.hasOwnProperty(k)) _mergeUpdate(original, k, v, options, _d + 1);
|
|
else _mergeInsertFix(original, k, v, options, _d + 1);
|
|
}
|
|
return original;
|
|
}
|
|
|
|
function _mergeInsertFix(original, k, v, { insertKeys, insertValues } = {}, _d) {
|
|
// Recursively create simple objects
|
|
if (v?.constructor === Object && insertKeys) {
|
|
original[k] = mergeObjectFix({}, v, { insertKeys: true, inplace: true });
|
|
return;
|
|
}
|
|
|
|
// Delete a key
|
|
if (k.startsWith('-=')) {
|
|
delete original[k.slice(2)];
|
|
return;
|
|
}
|
|
|
|
// Insert a key
|
|
const canInsert = (_d <= 1 && insertKeys) || (_d > 1 && insertValues);
|
|
if (canInsert) original[k] = v;
|
|
}
|
|
|
|
function _mergeUpdate(
|
|
original,
|
|
k,
|
|
v,
|
|
{ insertKeys, insertValues, enforceTypes, overwrite, recursive } = {},
|
|
_d
|
|
) {
|
|
const x = original[k];
|
|
const tv = getType(v);
|
|
const tx = getType(x);
|
|
|
|
// Recursively merge an inner object
|
|
if (tv === 'Object' && tx === 'Object' && recursive) {
|
|
return mergeObjectFix(
|
|
x,
|
|
v,
|
|
{
|
|
insertKeys: insertKeys,
|
|
insertValues: insertValues,
|
|
overwrite: overwrite,
|
|
inplace: true,
|
|
enforceTypes: enforceTypes,
|
|
},
|
|
_d
|
|
);
|
|
}
|
|
|
|
// Overwrite an existing value
|
|
if (overwrite) {
|
|
if (tx !== 'undefined' && tv !== tx && enforceTypes) {
|
|
throw new Error(`Mismatched data types encountered during object merge.`);
|
|
}
|
|
original[k] = v;
|
|
}
|
|
}
|