diff --git a/module/apps/OFTSettingsMenu.mjs b/module/apps/OFTSettingsMenu.mjs
new file mode 100644
index 0000000..1506c25
--- /dev/null
+++ b/module/apps/OFTSettingsMenu.mjs
@@ -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
+};
diff --git a/templates/OFTSettingsMenu/footer.hbs b/templates/OFTSettingsMenu/footer.hbs
new file mode 100644
index 0000000..41f2375
--- /dev/null
+++ b/templates/OFTSettingsMenu/footer.hbs
@@ -0,0 +1,8 @@
+
diff --git a/templates/OFTSettingsMenu/settingsList.hbs b/templates/OFTSettingsMenu/settingsList.hbs
new file mode 100644
index 0000000..2daeb00
--- /dev/null
+++ b/templates/OFTSettingsMenu/settingsList.hbs
@@ -0,0 +1,7 @@
+