Add basic support for registering custom icons that override icons provided by the system/modules

This commit is contained in:
Oliver 2026-02-09 23:06:51 -07:00
parent c90137b18f
commit c7541db1d9
9 changed files with 279 additions and 6 deletions

View file

@ -0,0 +1,132 @@
import { __ID__, filePath } from "../consts.mjs";
import { key as customStatusIconsKey } from "../tweaks/customStatusIcons.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
const { SettingsConfig } = foundry.applications.settings;
export class StatusEffectIconConfig extends HandlebarsApplicationMixin(ApplicationV2) {
// #region Options
static DEFAULT_OPTIONS = {
tag: `form`,
classes: [
__ID__,
`StatusEffectIconConfig`,
],
window: {
title: `OFT.apps.StatusEffectIconConfig.title`,
},
position: {
width: 550,
},
form: {
handler: this.#onSubmit,
closeOnSubmit: true,
submitOnChange: false,
},
actions: {
pickViaImageTagger: this.#pickViaImageTagger,
removeOverride: this.#removeOverride,
},
};
static PARTS = {
list: {
template: filePath(`templates/StatusEffectIconConfig/effects.hbs`),
scrollable: [``],
},
footer: {
template: filePath(`templates/StatusEffectIconConfig/footer.hbs`),
},
};
// #endregion Options
// #region Instance Data
#overrides = null;
#originalOverrides = null;
// #endregion Instance Data
// #region Lifecycle
async _onRender() {
const pickers = this.element.querySelectorAll(`file-picker`);
for (const picker of pickers) {
picker.addEventListener(`change`, this.#onChangeFilePicker.bind(this));
};
};
// #endregion Lifecycle
// #region Data Prep
_prepareContext() {
const ctx = {
meta: {
idp: this.id,
},
showImageTaggerButton: game.modules.get(`image-tagger`).active,
};
const effects = Object.values(CONFIG.statusEffects);
this.#overrides ??= game.settings.get(__ID__, customStatusIconsKey);
this.#originalOverrides ??= foundry.utils.deepClone(this.#overrides);
// console.log({ original: this.#originalOverrides, current: this.#overrides });
ctx.effects = [];
for (const effect of effects) {
let preview = this.#overrides[effect.id] ?? effect.img;
if (
this.#originalOverrides[effect.id] != null
&& this.#overrides[effect.id] === null
) {
preview = null;
}
ctx.effects.push({
id: effect.id,
preview,
name: game.i18n.localize(effect.name),
img: this.#overrides[effect.id],
hasOverride: this.#overrides[effect.id] != null,
});
};
return ctx;
};
// #endregion Data Prep
// #region Event Listeners
async #onChangeFilePicker(event) {
const target = event.currentTarget;
const id = target.closest(`[data-effect-id]`).dataset.effectId;
this.#overrides[id] = target.value || null;
await this.render();
};
/** @this {StatusEffectIconConfig} */
static async #onSubmit() {
game.settings.set(__ID__, customStatusIconsKey, this.#overrides);
SettingsConfig.reloadConfirm({ world: true });
};
// #endregion Event Listeners
// #region Actions
/** @this {StatusEffectIconConfig} */
static async #pickViaImageTagger(event, element) {
const id = element.closest(`[data-effect-id]`)?.dataset.effectId;
if (!id) { return };
const ArtBrowser = game.modules.get(`image-tagger`).api.Apps.ArtBrowser;
const newImage = await ArtBrowser.select();
if (!newImage) { return };
this.#overrides[id] = newImage;
await this.render();
};
/** @this {StatusEffectIconConfig} */
static async #removeOverride(event, element) {
const id = element.closest(`[data-effect-id]`)?.dataset.effectId;
this.#overrides[id] = null;
await this.render();
};
// #endregion Actions
};

View file

@ -4,6 +4,7 @@ import { addGlobalDocReferrer } from "../tweaks/addGlobalDocReferrer.mjs";
import { autoUnpauseOnLoad } from "../tweaks/autoUnpauseOnLoad.mjs";
import { chatImageLinks } from "../tweaks/chatImageLinks.mjs";
import { chatSidebarBackground } from "../tweaks/chatSidebarBackground.mjs";
import { customStatusIcons } from "../tweaks/customStatusIcons.mjs";
import { defaultHotbarPage } from "../tweaks/defaultHotbarPage.mjs";
import { hotbarButtonGap } from "../tweaks/hotbarButtonGap.mjs";
import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs";
@ -48,6 +49,7 @@ Hooks.on(`setup`, () => {
hotbarButtonGap();
repositionHotbar();
customStatusIcons();
chatImageLinks();
chatSidebarBackground();
startSidebarExpanded();

View file

@ -0,0 +1,44 @@
import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs";
import { __ID__ } from "../consts.mjs";
import { Logger } from "../utils/Logger.mjs";
import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs";
import { StatusEffectIconConfig } from "../apps/StatusEffectIconConfig.mjs";
export const key = `customStatusIcons`;
export function customStatusIcons() {
status[key] = SettingStatusEnum.Unknown;
if (preventTweakRegistration(key)) { return };
// #region Registration
Logger.log(`Registering tweak: ${key}`);
game.settings.registerMenu(__ID__, `${key}Menu`, {
name: `OFT.menu.${key}.name`,
hint: `OFT.menu.${key}.hint`,
label: `OFT.menu.${key}.label`,
restricted: true,
type: StatusEffectIconConfig,
});
game.settings.register(__ID__, key, {
scope: `world`,
config: false,
type: Object,
default: {},
});
// #endregion Registration
// #region Implementation
Hooks.on(`ready`, () => {
const value = game.settings.get(__ID__, key);
const effects = Object.values(CONFIG.statusEffects);
for (const effect of effects) {
if (value[effect.id] != null) {
effect.img = value[effect.id];
};
};
});
// #endregion Implementation
status[key] = SettingStatusEnum.Registered;
};