Merge pull request 'Add ability to select custom status icons' (#44) from feature/custom-status-icons into main
Reviewed-on: #44
This commit is contained in:
commit
c86a7b13cd
17 changed files with 562 additions and 7 deletions
1
assets/icons/save.svg
Normal file
1
assets/icons/save.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="m56.133 34.207 8.082-.004a2.414 2.414 0 0 0 2.41-2.41l-.012-16.227A2.406 2.406 0 0 0 64.2 13.16l-8.078.004a2.41 2.41 0 0 0-2.41 2.41l.012 16.23a2.41 2.41 0 0 0 2.41 2.402z"/><path d="M92.758 22.266c-.004-1.555-.375-2.832-1.379-3.828-.644-.645-7.7-7.691-11.328-11.316C78.774 5.852 76.371 5 74.582 5s-60.258.039-60.258.039A7.13 7.13 0 0 0 7.2 12.173l.051 75.703A7.13 7.13 0 0 0 14.383 95l71.289-.047a7.126 7.126 0 0 0 7.125-7.133c.004 0-.04-64.594-.04-65.555zM71.02 9.414l.016 24.371c0 2.117-2.18 4.148-4.297 4.148l-30.977.02c-2.117 0-4.106-2.027-4.106-4.145l-.015-24.367zm9.781 78.281H19.2V53.652c0-2.562 1.992-4.644 4.45-4.644h52.7c2.461 0 4.45 2.078 4.45 4.644z"/></svg>
|
||||||
|
After Width: | Height: | Size: 742 B |
|
|
@ -62,6 +62,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu": {
|
"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": {
|
"devSettings": {
|
||||||
"name": "Developer Settings",
|
"name": "Developer Settings",
|
||||||
"hint": "Tweaks that are relevant if you are developing something within Foundry, but are rarely useful outside of that context.",
|
"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": {
|
"apps": {
|
||||||
"no-settings-to-display": "No settings to display",
|
"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": {
|
"notifs": {
|
||||||
"toggleMouseBroadcast": {
|
"toggleMouseBroadcast": {
|
||||||
|
|
|
||||||
131
module/apps/StatusEffectIconConfig.mjs
Normal file
131
module/apps/StatusEffectIconConfig.mjs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
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 ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const effects = Object.values(CONFIG.statusEffects);
|
||||||
|
|
||||||
|
this.#overrides ??= game.settings.get(__ID__, customStatusIconsKey);
|
||||||
|
this.#originalOverrides ??= foundry.utils.deepClone(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
|
||||||
|
};
|
||||||
11
module/apps/elements/Icon.mjs
Normal file
11
module/apps/elements/Icon.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { OFTSVGLoader } from "./SVGLoader.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attributes:
|
||||||
|
@property {string} name - The name of the icon, takes precedence over the path
|
||||||
|
@property {string} path - The path of the icon file
|
||||||
|
*/
|
||||||
|
export class OFTIcon extends OFTSVGLoader {
|
||||||
|
static elementName = `oft-icon`;
|
||||||
|
static _stylePath = `icon.css`;
|
||||||
|
};
|
||||||
102
module/apps/elements/SVGLoader.mjs
Normal file
102
module/apps/elements/SVGLoader.mjs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { filePath } from "../../consts.mjs";
|
||||||
|
import { StyledShadowElement } from "./StyledShadowElement.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attributes:
|
||||||
|
@property {string} name - The name of the icon, takes precedence over the path
|
||||||
|
@property {string} path - The path of the icon file
|
||||||
|
*/
|
||||||
|
export class OFTSVGLoader extends StyledShadowElement(HTMLElement) {
|
||||||
|
static elementName = `oft-svg`;
|
||||||
|
static formAssociated = false;
|
||||||
|
|
||||||
|
/* Stuff for the mixin to use */
|
||||||
|
static _stylePath = `svg-loader.css`;
|
||||||
|
|
||||||
|
|
||||||
|
static _cache = new Map();
|
||||||
|
#container;
|
||||||
|
/** @type {null | string} */
|
||||||
|
_name;
|
||||||
|
/** @type {null | string} */
|
||||||
|
_path;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.#container = document.createElement(`div`);
|
||||||
|
this._shadow.appendChild(this.#container);
|
||||||
|
};
|
||||||
|
|
||||||
|
_mounted = false;
|
||||||
|
async connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
if (this._mounted) { return };
|
||||||
|
|
||||||
|
this._name = this.getAttribute(`name`);
|
||||||
|
this._path = this.getAttribute(`path`);
|
||||||
|
|
||||||
|
/*
|
||||||
|
This converts all of the double-dash prefixed properties on the element to
|
||||||
|
CSS variables so that they don't all need to be provided by doing style=""
|
||||||
|
*/
|
||||||
|
for (const attrVar of this.attributes) {
|
||||||
|
if (attrVar.name?.startsWith(`var:`)) {
|
||||||
|
const prop = attrVar.name.replace(`var:`, ``);
|
||||||
|
this.style.setProperty(`--` + prop, attrVar.value);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Try to retrieve the icon if it isn't present, try the path then default to
|
||||||
|
the slot content, as then we can have a default per-icon usage
|
||||||
|
*/
|
||||||
|
let content;
|
||||||
|
if (this._name) {
|
||||||
|
content = await this.#getIcon(filePath(`assets/${this._name}.svg`));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._path && !content) {
|
||||||
|
content = await this.#getIcon(this._path);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
this.#container.appendChild(content.cloneNode(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
this._mounted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (!this._mounted) { return };
|
||||||
|
|
||||||
|
this._mounted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
async #getIcon(path) {
|
||||||
|
// Cache hit!
|
||||||
|
if (this.constructor._cache.has(path)) {
|
||||||
|
return this.constructor._cache.get(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const r = await fetch(path);
|
||||||
|
switch (r.status) {
|
||||||
|
case 200:
|
||||||
|
case 201:
|
||||||
|
break;
|
||||||
|
default: return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const svg = this.#parseSVG(await r.text());
|
||||||
|
this.constructor._cache.set(path, svg);
|
||||||
|
return svg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Takes an SVG string and returns it as a DOM node */
|
||||||
|
#parseSVG(content) {
|
||||||
|
const temp = document.createElement(`div`);
|
||||||
|
temp.innerHTML = content;
|
||||||
|
return temp.querySelector(`svg`);
|
||||||
|
};
|
||||||
|
};
|
||||||
66
module/apps/elements/StyledShadowElement.mjs
Normal file
66
module/apps/elements/StyledShadowElement.mjs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { filePath } from "../../consts.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} Base
|
||||||
|
*/
|
||||||
|
export function StyledShadowElement(Base) {
|
||||||
|
return class extends Base {
|
||||||
|
/**
|
||||||
|
* The path to the CSS that is loaded
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
static _stylePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stringified CSS to use
|
||||||
|
* @type {Map<string, string>}
|
||||||
|
*/
|
||||||
|
static _styles = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTML element of the stylesheet
|
||||||
|
* @type {HTMLStyleElement}
|
||||||
|
*/
|
||||||
|
_style;
|
||||||
|
|
||||||
|
/** @type {ShadowRoot} */
|
||||||
|
_shadow;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._shadow = this.attachShadow({ mode: `open` });
|
||||||
|
this._style = document.createElement(`style`);
|
||||||
|
this._shadow.appendChild(this._style);
|
||||||
|
};
|
||||||
|
|
||||||
|
#mounted = false;
|
||||||
|
connectedCallback() {
|
||||||
|
if (this.#mounted) { return };
|
||||||
|
|
||||||
|
this._getStyles();
|
||||||
|
|
||||||
|
this.#mounted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (!this.#mounted) { return };
|
||||||
|
this.#mounted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
_getStyles() {
|
||||||
|
// TODO: Cache the CSS content in a more sane way that doesn't break
|
||||||
|
const stylePath = this.constructor._stylePath;
|
||||||
|
if (this.constructor._styles.has(stylePath)) {
|
||||||
|
this._style.innerHTML = this.constructor._styles.get(stylePath);
|
||||||
|
} else {
|
||||||
|
fetch(filePath(`styles/components/${stylePath}`))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => {
|
||||||
|
this.constructor._styles.set(stylePath, t);
|
||||||
|
this._style.innerHTML = t;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
22
module/apps/elements/_index.mjs
Normal file
22
module/apps/elements/_index.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { OFTIcon } from "./Icon.mjs";
|
||||||
|
import { OFTSVGLoader } from "./SVGLoader.mjs";
|
||||||
|
|
||||||
|
const components = [
|
||||||
|
OFTSVGLoader,
|
||||||
|
OFTIcon,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function registerCustomComponents() {
|
||||||
|
(CONFIG.CACHE ??= {}).componentListeners ??= [];
|
||||||
|
for (const component of components) {
|
||||||
|
if (!window.customElements.get(component.elementName)) {
|
||||||
|
window.customElements.define(
|
||||||
|
component.elementName,
|
||||||
|
component,
|
||||||
|
);
|
||||||
|
if (component.formAssociated) {
|
||||||
|
CONFIG.CACHE.componentListeners.push(component.elementName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
// Settings
|
// Tweaks
|
||||||
import { preventMovementHistory } from "../tweaks/preventMovementHistory.mjs";
|
import { preventMovementHistory } from "../tweaks/preventMovementHistory.mjs";
|
||||||
import { toggleMouseBroadcast } from "../tweaks/toggleMouseBroadcast.mjs";
|
import { toggleMouseBroadcast } from "../tweaks/toggleMouseBroadcast.mjs";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { Logger } from "../utils/Logger.mjs";
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
import { registerCustomComponents } from "../apps/elements/_index.mjs";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This is only here for setting that **require** being registered during
|
This is only here for setting that **require** being registered during
|
||||||
|
|
@ -14,6 +15,8 @@ where they ideally should be implemented.
|
||||||
Hooks.on(`init`, () => {
|
Hooks.on(`init`, () => {
|
||||||
Logger.log(`Initializing`);
|
Logger.log(`Initializing`);
|
||||||
|
|
||||||
|
registerCustomComponents();
|
||||||
|
|
||||||
preventMovementHistory();
|
preventMovementHistory();
|
||||||
toggleMouseBroadcast();
|
toggleMouseBroadcast();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { addGlobalDocReferrer } from "../tweaks/addGlobalDocReferrer.mjs";
|
||||||
import { autoUnpauseOnLoad } from "../tweaks/autoUnpauseOnLoad.mjs";
|
import { autoUnpauseOnLoad } from "../tweaks/autoUnpauseOnLoad.mjs";
|
||||||
import { chatImageLinks } from "../tweaks/chatImageLinks.mjs";
|
import { chatImageLinks } from "../tweaks/chatImageLinks.mjs";
|
||||||
import { chatSidebarBackground } from "../tweaks/chatSidebarBackground.mjs";
|
import { chatSidebarBackground } from "../tweaks/chatSidebarBackground.mjs";
|
||||||
|
import { customStatusIcons } from "../tweaks/customStatusIcons.mjs";
|
||||||
import { defaultHotbarPage } from "../tweaks/defaultHotbarPage.mjs";
|
import { defaultHotbarPage } from "../tweaks/defaultHotbarPage.mjs";
|
||||||
import { hotbarButtonGap } from "../tweaks/hotbarButtonGap.mjs";
|
import { hotbarButtonGap } from "../tweaks/hotbarButtonGap.mjs";
|
||||||
import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs";
|
import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs";
|
||||||
|
|
@ -48,6 +49,7 @@ Hooks.on(`setup`, () => {
|
||||||
hotbarButtonGap();
|
hotbarButtonGap();
|
||||||
repositionHotbar();
|
repositionHotbar();
|
||||||
|
|
||||||
|
customStatusIcons();
|
||||||
chatImageLinks();
|
chatImageLinks();
|
||||||
chatSidebarBackground();
|
chatSidebarBackground();
|
||||||
startSidebarExpanded();
|
startSidebarExpanded();
|
||||||
|
|
|
||||||
44
module/tweaks/customStatusIcons.mjs
Normal file
44
module/tweaks/customStatusIcons.mjs
Normal 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;
|
||||||
|
};
|
||||||
59
styles/apps/StatusEffectIconConfig.css
Normal file
59
styles/apps/StatusEffectIconConfig.css
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
.oft.StatusEffectIconConfig {
|
||||||
|
> .window-content {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
height: initial;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
styles/components/icon.css
Normal file
23
styles/components/icon.css
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: var(--size, 1rem);
|
||||||
|
height: var(--size, 1rem);
|
||||||
|
fill: var(--fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
stroke: var(--stroke);
|
||||||
|
stroke-width: var(--stroke-width);
|
||||||
|
stroke-linejoin: var(--stroke-linejoin);
|
||||||
|
}
|
||||||
22
styles/components/svg-loader.css
Normal file
22
styles/components/svg-loader.css
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--fill);
|
||||||
|
stroke: var(--stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
stroke: var(--stroke);
|
||||||
|
stroke-width: var(--stroke-width);
|
||||||
|
stroke-linejoin: var(--stroke-linejoin);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
@import url("./chatSidebarBackground.css");
|
@layer resets, elements, tweaks, apps;
|
||||||
@import url("./hotbarButtonGap.css");
|
|
||||||
@import url("./hotbarButtonSize.css");
|
|
||||||
@import url("./repositionHotbar.css");
|
|
||||||
|
|
||||||
@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 */
|
/* Make the chat sidebar the same width as all the other tabs */
|
||||||
.chat-sidebar:not(.sidebar-popout) { width: var(--sidebar-width); }
|
.chat-sidebar:not(.sidebar-popout) { width: var(--sidebar-width); }
|
||||||
|
|
|
||||||
44
templates/StatusEffectIconConfig/effects.hbs
Normal file
44
templates/StatusEffectIconConfig/effects.hbs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<main class="scrollable">
|
||||||
|
{{#if effects}}
|
||||||
|
<ul class="effect-list">
|
||||||
|
{{#each effects as | effect |}}
|
||||||
|
<li class="effect" data-effect-id="{{ effect.id }}">
|
||||||
|
<div class="preview">
|
||||||
|
{{#if effect.preview}}
|
||||||
|
<img
|
||||||
|
src="{{effect.preview}}"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<h2>{{ effect.name }}</h2>
|
||||||
|
<file-picker
|
||||||
|
value="{{effect.img}}"
|
||||||
|
></file-picker>
|
||||||
|
{{#if @root.showImageTaggerButton}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon fa-solid fa-paintbrush"
|
||||||
|
aria-label="{{localize "OFT.apps.StatusEffectIconConfig.select-using-image-tagger"}}"
|
||||||
|
data-tooltip
|
||||||
|
data-action="pickViaImageTagger"
|
||||||
|
></button>
|
||||||
|
{{/if}}
|
||||||
|
{{#if effect.hasOverride}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon fa-solid fa-xmark"
|
||||||
|
aria-label="{{localize "OFT.apps.StatusEffectIconConfig.remove-override"}}"
|
||||||
|
data-tooltip
|
||||||
|
data-action="removeOverride"
|
||||||
|
></button>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{else}}
|
||||||
|
<div class="placeholder">
|
||||||
|
{{ localize "OFT.apps.StatusEffectIconConfig.no-status-effects" }}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</main>
|
||||||
11
templates/StatusEffectIconConfig/footer.hbs
Normal file
11
templates/StatusEffectIconConfig/footer.hbs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<footer>
|
||||||
|
<button type="submit">
|
||||||
|
<oft-icon
|
||||||
|
name="icons/save"
|
||||||
|
aria-hidden="true"
|
||||||
|
var:fill="currentColor"
|
||||||
|
var:size="1.25rem"
|
||||||
|
></oft-icon>
|
||||||
|
{{ localize "Save Changes" }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue