Add a generic helper app to display settings menus similar to how Foundry does by default, but without the category selector
This commit is contained in:
parent
523d9c1da2
commit
28d0105397
3 changed files with 200 additions and 0 deletions
185
module/apps/OFTSettingsMenu.mjs
Normal file
185
module/apps/OFTSettingsMenu.mjs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { filePath } from "../consts.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin: HAM, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
export class OFTSettingsMenu extends HAM(ApplicationV2) {
|
||||||
|
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
tag: `form`,
|
||||||
|
window: {
|
||||||
|
icon: `fa-solid fa-gears`,
|
||||||
|
resizable: true,
|
||||||
|
contentClasses: [
|
||||||
|
`standard-form`,
|
||||||
|
],
|
||||||
|
controls: [
|
||||||
|
{
|
||||||
|
icon: `fa-solid fa-arrow-rotate-left`,
|
||||||
|
label: `PACKAGECONFIG.Reset`,
|
||||||
|
visible: true,
|
||||||
|
action: `resetDefaults`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
handler: this.#handleSubmit,
|
||||||
|
closeOnSubmit: true,
|
||||||
|
submitOnChange: false,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 544,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
resetDefaults: this.#resetDefaults,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
settingsList: {
|
||||||
|
template: filePath(`templates/OFTSettingsMenu/settingsList.hbs`),
|
||||||
|
scrollable: [``],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
template: filePath(`templates/OFTSettingsMenu/footer.hbs`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static _SETTINGS = [];
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Data Prep
|
||||||
|
async _prepareContext() {
|
||||||
|
return {
|
||||||
|
meta: {
|
||||||
|
idp: this.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#partContextPrep = {
|
||||||
|
settingsList: this._prepareSettingsListContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
async _preparePartContext(partID, ctx) {
|
||||||
|
ctx = foundry.utils.deepClone(ctx);
|
||||||
|
|
||||||
|
const prepper = this.#partContextPrep[partID];
|
||||||
|
if (prepper && typeof prepper === `function`) {
|
||||||
|
await prepper.call(this, ctx);
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareSettingsListContext(ctx) {
|
||||||
|
const canConfigure = game.user.can(`SETTINGS_MODIFY`);
|
||||||
|
ctx.settings = [];
|
||||||
|
|
||||||
|
const settingIDs = this.constructor._SETTINGS;
|
||||||
|
for ( const settingID of settingIDs ) {
|
||||||
|
const setting = game.settings.settings.get(settingID);
|
||||||
|
if ( !canConfigure && setting.scope === CONST.SETTING_SCOPES.WORLD ) { continue }
|
||||||
|
const data = {
|
||||||
|
label: setting.value,
|
||||||
|
value: game.settings.get(setting.namespace, setting.key),
|
||||||
|
menu: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define a DataField for each setting not originally defined with one
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
if ( setting.type instanceof fields.DataField ) {
|
||||||
|
data.field = setting.type;
|
||||||
|
}
|
||||||
|
else if ( setting.type === Boolean ) {
|
||||||
|
data.field = new fields.BooleanField({initial: setting.default ?? false});
|
||||||
|
}
|
||||||
|
else if ( setting.type === Number ) {
|
||||||
|
const {min, max, step} = setting.range ?? {};
|
||||||
|
data.field = new fields.NumberField({
|
||||||
|
required: true,
|
||||||
|
choices: setting.choices,
|
||||||
|
initial: setting.default,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if ( setting.filePicker ) {
|
||||||
|
const categories = {
|
||||||
|
audio: [`AUDIO`],
|
||||||
|
folder: [],
|
||||||
|
font: [`FONT`],
|
||||||
|
graphics: [`GRAPHICS`],
|
||||||
|
image: [`IMAGE`],
|
||||||
|
imagevideo: [`IMAGE`, `VIDEO`],
|
||||||
|
text: [`TEXT`],
|
||||||
|
video: [`VIDEO`],
|
||||||
|
}[setting.filePicker] ?? Object.keys(CONST.FILE_CATEGORIES).filter(c => c !== `HTML`);
|
||||||
|
if ( categories.length ) {
|
||||||
|
data.field = new fields.FilePathField({required: true, blank: true, categories});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data.field = new fields.StringField({required: true}); // Folder paths cannot be FilePathFields
|
||||||
|
data.folderPicker = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data.field = new fields.StringField({required: true, choices: setting.choices});
|
||||||
|
}
|
||||||
|
data.field.name = `${setting.namespace}.${setting.key}`;
|
||||||
|
data.field.label ||= game.i18n.localize(setting.name ?? ``);
|
||||||
|
data.field.hint ||= game.i18n.localize(setting.hint ?? ``);
|
||||||
|
|
||||||
|
// Categorize setting
|
||||||
|
ctx.settings.push(data);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// #endregion Data Prep
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
/** @this {OFTSettingsMenu} */
|
||||||
|
static async #handleSubmit(_event, _form, formData) {
|
||||||
|
let requiresClientReload = false;
|
||||||
|
let requiresWorldReload = false;
|
||||||
|
for ( const [key, value] of Object.entries(formData.object) ) {
|
||||||
|
const setting = game.settings.settings.get(key);
|
||||||
|
if ( !setting ) { continue };
|
||||||
|
const priorValue = game.settings.get(setting.namespace, setting.key, {document: true})?._source.value;
|
||||||
|
let newSetting;
|
||||||
|
try {
|
||||||
|
newSetting = await game.settings.set(setting.namespace, setting.key, value, {document: true});
|
||||||
|
} catch(error) {
|
||||||
|
ui.notifications.error(error);
|
||||||
|
}
|
||||||
|
if ( priorValue === newSetting?._source.value ) { continue }; // Compare JSON strings
|
||||||
|
requiresClientReload ||= (setting.scope !== CONST.SETTING_SCOPES.WORLD) && setting.requiresReload;
|
||||||
|
requiresWorldReload ||= (setting.scope === CONST.SETTING_SCOPES.WORLD) && setting.requiresReload;
|
||||||
|
}
|
||||||
|
if ( requiresClientReload || requiresWorldReload ) {
|
||||||
|
await foundry.applications.settings.SettingsConfig.reloadConfirm({world: requiresWorldReload});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {OFTSettingsMenu} */
|
||||||
|
static async #resetDefaults() {
|
||||||
|
const form = this.form;
|
||||||
|
for ( const settingID of this.constructor._SETTINGS ) {
|
||||||
|
const setting = game.settings.settings.get(settingID);
|
||||||
|
console.log({ settingID, setting });
|
||||||
|
const input = form[settingID];
|
||||||
|
if ( !input || !setting ) { continue };
|
||||||
|
|
||||||
|
if ( input.type === `checkbox` ) {
|
||||||
|
input.checked = setting.default;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input.value = setting.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event(`change`));
|
||||||
|
}
|
||||||
|
ui.notifications.info(`SETTINGS.ResetInfo`, {localize: true});
|
||||||
|
};
|
||||||
|
// #endregion Actions
|
||||||
|
};
|
||||||
8
templates/OFTSettingsMenu/footer.hbs
Normal file
8
templates/OFTSettingsMenu/footer.hbs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<footer class="form-footer">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-floppy-disk" inert aria-hidden="true"></i>
|
||||||
|
{{localize "SETTINGS.Save"}}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
7
templates/OFTSettingsMenu/settingsList.hbs
Normal file
7
templates/OFTSettingsMenu/settingsList.hbs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<section class="setting-list scrollable">
|
||||||
|
{{#each settings as |setting|}}
|
||||||
|
{{formGroup setting.field value=setting.value rootId=@root.meta.idp localize=true }}
|
||||||
|
{{else}}
|
||||||
|
{{localize "OFT.apps.no-settings-to-display"}}
|
||||||
|
{{/each}}
|
||||||
|
</section>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue