diff --git a/langs/en-ca.json b/langs/en-ca.json index b744059..a64ce8d 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -62,6 +62,11 @@ } }, "menu": { + "customStatusIcons": { + "name": "Custom Status Icons", + "hint": "(v13+) Modify your token status effect icons without needing a custom module or a world script to do so! Pick any image on your server for the icons!", + "label": "Configure Icons" + }, "devSettings": { "name": "Developer Settings", "hint": "Tweaks that are relevant if you are developing something within Foundry, but are rarely useful outside of that context.", @@ -81,7 +86,13 @@ }, "apps": { "no-settings-to-display": "No settings to display", - "make-global-reference": "Make Global Reference" + "make-global-reference": "Make Global Reference", + "StatusEffectIconConfig": { + "title": "Configure Status Effect Icons", + "no-status-effects": "No status effects detected, this is most likely due to your game system or other modules.", + "remove-override": "Remove custom override", + "select-using-image-tagger": "Select using Image Tagger" + } }, "notifs": { "toggleMouseBroadcast": { diff --git a/module/apps/StatusEffectIconConfig.mjs b/module/apps/StatusEffectIconConfig.mjs new file mode 100644 index 0000000..25b3df0 --- /dev/null +++ b/module/apps/StatusEffectIconConfig.mjs @@ -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 +}; diff --git a/module/hooks/setup.mjs b/module/hooks/setup.mjs index d693714..b3d5fbf 100644 --- a/module/hooks/setup.mjs +++ b/module/hooks/setup.mjs @@ -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(); diff --git a/module/tweaks/customStatusIcons.mjs b/module/tweaks/customStatusIcons.mjs new file mode 100644 index 0000000..00a325b --- /dev/null +++ b/module/tweaks/customStatusIcons.mjs @@ -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; +}; diff --git a/styles/apps/StatusEffectIconConfig.css b/styles/apps/StatusEffectIconConfig.css new file mode 100644 index 0000000..4298e7c --- /dev/null +++ b/styles/apps/StatusEffectIconConfig.css @@ -0,0 +1,34 @@ +.oft.StatusEffectIconConfig { + .effect-list { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 16px; + } + + .effect { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + margin: 0; + + .preview { + --size: 30px; + width: var(--size); + height: var(--size); + display: flex; + justify-content: center; + align-items: center; + } + + h2 { + margin: 0; + font-size: 1rem; + font-family: var(--font-body); + flex-grow: 1; + } + } +} diff --git a/styles/apps.css b/styles/apps/common.css similarity index 100% rename from styles/apps.css rename to styles/apps/common.css diff --git a/styles/main.css b/styles/main.css index 1d0fec5..63794a5 100644 --- a/styles/main.css +++ b/styles/main.css @@ -1,9 +1,12 @@ -@import url("./chatSidebarBackground.css"); -@import url("./hotbarButtonGap.css"); -@import url("./hotbarButtonSize.css"); -@import url("./repositionHotbar.css"); +@layer resets, elements, tweaks, apps; -@import url("./apps.css"); +@import url("./chatSidebarBackground.css") layer(tweaks); +@import url("./hotbarButtonGap.css") layer(tweaks); +@import url("./hotbarButtonSize.css") layer(tweaks); +@import url("./repositionHotbar.css") layer(tweaks); + +@import url("./apps/common.css") layer(apps); +@import url("./apps/StatusEffectIconConfig.css") layer(apps); /* Make the chat sidebar the same width as all the other tabs */ .chat-sidebar:not(.sidebar-popout) { width: var(--sidebar-width); } diff --git a/templates/StatusEffectIconConfig/effects.hbs b/templates/StatusEffectIconConfig/effects.hbs new file mode 100644 index 0000000..dffda0e --- /dev/null +++ b/templates/StatusEffectIconConfig/effects.hbs @@ -0,0 +1,42 @@ +