feat: Implement a movable and detachable Daggerheart countdown tracker with UI and module integration.
This commit is contained in:
commit
8da4684488
4 changed files with 302 additions and 0 deletions
33
module.json
Normal file
33
module.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"id": "dh-countdownsplus",
|
||||||
|
"title": "Daggerheart Countdowns Plus",
|
||||||
|
"description": "A module to allow the Daggerheart countdown tracker to be detached and moved freely.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"compatibility": {
|
||||||
|
"minimum": "12",
|
||||||
|
"verified": "12"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Antigravity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": {
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"id": "daggerheart",
|
||||||
|
"type": "system",
|
||||||
|
"compatibility": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"esmodules": [
|
||||||
|
"scripts/module.js"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"styles/module.css"
|
||||||
|
],
|
||||||
|
"url": "",
|
||||||
|
"manifest": "",
|
||||||
|
"download": ""
|
||||||
|
}
|
||||||
144
scripts/MovableCountdowns.js
Normal file
144
scripts/MovableCountdowns.js
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
/**
|
||||||
|
* Creates the MovableCountdowns class extending the provided base class.
|
||||||
|
* @param {class} BaseCountdowns The DhCountdowns class from Daggerheart system.
|
||||||
|
* @returns {class} The MovableCountdowns class.
|
||||||
|
*/
|
||||||
|
export function createMovableCountdownsClass(BaseCountdowns) {
|
||||||
|
return class MovableCountdowns extends BaseCountdowns {
|
||||||
|
constructor(options = {}) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
id: 'movable-countdowns',
|
||||||
|
tag: 'div', // AppV2
|
||||||
|
classes: ['daggerheart', 'dh-style', 'countdowns', 'movable-countdowns'],
|
||||||
|
window: {
|
||||||
|
icon: 'fa-solid fa-clock-rotate-left',
|
||||||
|
frame: true,
|
||||||
|
title: 'DAGGERHEART.UI.Countdowns.title',
|
||||||
|
positioned: true,
|
||||||
|
resizable: true,
|
||||||
|
minimizable: true
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// Override toggleViewMode to instance method
|
||||||
|
toggleViewMode: MovableCountdowns.prototype._toggleViewMode,
|
||||||
|
|
||||||
|
// Inherited static actions
|
||||||
|
editCountdowns: BaseCountdowns.DEFAULT_OPTIONS.actions.editCountdowns,
|
||||||
|
loopCountdown: BaseCountdowns.DEFAULT_OPTIONS.actions.loopCountdown,
|
||||||
|
decreaseCountdown: BaseCountdowns.DEFAULT_OPTIONS.actions.decreaseCountdown,
|
||||||
|
increaseCountdown: BaseCountdowns.DEFAULT_OPTIONS.actions.increaseCountdown,
|
||||||
|
|
||||||
|
reattach: MovableCountdowns.reattach
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 320,
|
||||||
|
height: 'auto',
|
||||||
|
top: 100,
|
||||||
|
left: 100
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static PARTS = {
|
||||||
|
resources: {
|
||||||
|
root: true,
|
||||||
|
template: 'systems/daggerheart/templates/ui/countdowns.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get element() {
|
||||||
|
return document.getElementById(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-implementation of toggleViewMode that works on this instance.
|
||||||
|
*/
|
||||||
|
async _toggleViewMode() {
|
||||||
|
// Need access to system CONFIG constants.
|
||||||
|
// CONFIG.DH is global.
|
||||||
|
const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode);
|
||||||
|
const appMode = CONFIG.DH.GENERAL.countdownAppMode;
|
||||||
|
const newMode = currentMode === appMode.textIcon ? appMode.iconOnly : appMode.textIcon;
|
||||||
|
|
||||||
|
await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode, newMode);
|
||||||
|
|
||||||
|
// Update this instance directly
|
||||||
|
if (newMode === appMode.iconOnly) this.element.classList.add('icon-only');
|
||||||
|
else this.element.classList.remove('icon-only');
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
// Note: System tracker won't update automatically here if it's hidden,
|
||||||
|
// but next time it renders it will check flag.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**@inheritdoc */
|
||||||
|
/**@inheritdoc */
|
||||||
|
async _renderFrame(options) {
|
||||||
|
const frame = await super._renderFrame(options);
|
||||||
|
|
||||||
|
const header = frame.querySelector('.window-header');
|
||||||
|
if (!header) return frame;
|
||||||
|
|
||||||
|
// Re-add Close Button if missing
|
||||||
|
// dh-countdownsplus: Fix invisible close button by using <a> tag pattern like system
|
||||||
|
if (!header.querySelector('[data-action="close"]')) {
|
||||||
|
const closeTitle = game.i18n.localize("CLOSE");
|
||||||
|
// Use <a> with <i> child to match system header control styling
|
||||||
|
const closeBtn = document.createElement('a');
|
||||||
|
closeBtn.className = 'header-control';
|
||||||
|
closeBtn.setAttribute('data-action', 'close');
|
||||||
|
closeBtn.setAttribute('data-tooltip', closeTitle);
|
||||||
|
closeBtn.setAttribute('aria-label', closeTitle);
|
||||||
|
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.className = 'fa-solid fa-xmark';
|
||||||
|
closeBtn.appendChild(icon);
|
||||||
|
|
||||||
|
header.appendChild(closeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async _onRender(context, options) {
|
||||||
|
// Call super to handle template rendering
|
||||||
|
await super._onRender(context, options);
|
||||||
|
|
||||||
|
// Workaround system pinning: Move back to body if pinned
|
||||||
|
if (this.element && this.element.parentElement && this.element.parentElement.id === 'ui-right-column-1') {
|
||||||
|
document.body.appendChild(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure visibility
|
||||||
|
this.element.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async reattach() {
|
||||||
|
const movableApp = Object.values(ui.windows).find(w => w instanceof MovableCountdowns);
|
||||||
|
if (movableApp) await movableApp.close();
|
||||||
|
|
||||||
|
// Unsuppress system tracker
|
||||||
|
if (ui.countdowns) {
|
||||||
|
ui.countdowns._suppressed = false;
|
||||||
|
await ui.countdowns.render({ force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
/** @override */
|
||||||
|
async close(options = {}) {
|
||||||
|
await super.close(options);
|
||||||
|
// Re-attach on close
|
||||||
|
if (ui.countdowns) {
|
||||||
|
ui.countdowns._suppressed = false;
|
||||||
|
await ui.countdowns.render({ force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
101
scripts/module.js
Normal file
101
scripts/module.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { createMovableCountdownsClass } from './MovableCountdowns.js';
|
||||||
|
|
||||||
|
const MODULE_ID = 'dh-countdownsplus';
|
||||||
|
let MovableCountdowns;
|
||||||
|
|
||||||
|
Hooks.once('init', () => {
|
||||||
|
console.log(`${MODULE_ID} | Initializing Daggerheart Countdowns Plus`);
|
||||||
|
|
||||||
|
// Register settings or other init logic if needed
|
||||||
|
});
|
||||||
|
|
||||||
|
Hooks.on('ready', () => {
|
||||||
|
// Ensure the system's countdown class exists
|
||||||
|
if (!CONFIG.ui.countdowns) {
|
||||||
|
console.error(`${MODULE_ID} | CONFIG.ui.countdowns not found. Is Daggerheart system initialized?`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create our class extending the system's class
|
||||||
|
MovableCountdowns = createMovableCountdownsClass(CONFIG.ui.countdowns);
|
||||||
|
|
||||||
|
// Initial check to see if we should inject button
|
||||||
|
hookSystemCountdowns();
|
||||||
|
});
|
||||||
|
|
||||||
|
function hookSystemCountdowns() {
|
||||||
|
// We want to hook into the render of the system's countdown app.
|
||||||
|
// The system app is likely a singleton at ui.countdowns.
|
||||||
|
|
||||||
|
// Hook on renderDhCountdowns (name of the class in system is DhCountdowns)
|
||||||
|
Hooks.on('renderDhCountdowns', (app, html, data) => {
|
||||||
|
// Only inject if this is the SYSTEM one, not our movable one (ours inherits, so it might trigger this hook too?)
|
||||||
|
// DhCountdowns extends ApplicationV2. AppV2 emits 'renderApplicationV2' and 'renderMyClass'.
|
||||||
|
// So 'renderDhCountdowns' will fire for both.
|
||||||
|
|
||||||
|
if (app instanceof MovableCountdowns) return;
|
||||||
|
|
||||||
|
// Find the header to inject button
|
||||||
|
// html is the HTMLElement in AppV2 hooks
|
||||||
|
const header = html.querySelector('.window-header');
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
// Check if button already exists
|
||||||
|
if (header.querySelector('[data-action="detach-countdowns"]')) return;
|
||||||
|
|
||||||
|
// Create Detach Button
|
||||||
|
const detachTooltip = "Detach"; // TODO: Localize
|
||||||
|
const detachBtn = document.createElement('a'); // System uses <a> for header controls
|
||||||
|
detachBtn.className = 'header-control';
|
||||||
|
detachBtn.dataset.tooltip = detachTooltip;
|
||||||
|
detachBtn.dataset.action = 'detach-countdowns';
|
||||||
|
detachBtn.innerHTML = '<i class="fa-solid fa-up-right-from-square"></i>';
|
||||||
|
|
||||||
|
// Add listener
|
||||||
|
detachBtn.addEventListener('click', async (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
// Open Movable
|
||||||
|
// We assume singleton pattern for movable too
|
||||||
|
let movable = Object.values(ui.windows).find(w => w instanceof MovableCountdowns);
|
||||||
|
if (!movable) {
|
||||||
|
movable = new MovableCountdowns();
|
||||||
|
}
|
||||||
|
await movable.render({ force: true });
|
||||||
|
|
||||||
|
// Close/Hide System App
|
||||||
|
// Suppress future renders
|
||||||
|
if (app) {
|
||||||
|
app._suppressed = true;
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert before the "Close" button? System one doesn't have close button.
|
||||||
|
// Insert at end or specific position?
|
||||||
|
// System header controls: Edit, Toggle Mode.
|
||||||
|
// Let's prepend or append.
|
||||||
|
header.insertAdjacentElement('beforeend', detachBtn);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Patch system render to respect suppression
|
||||||
|
if (ui.countdowns && !ui.countdowns._patched) {
|
||||||
|
const originalRender = ui.countdowns.render.bind(ui.countdowns);
|
||||||
|
ui.countdowns.render = function (options, ...args) {
|
||||||
|
if (this._suppressed) {
|
||||||
|
// Update internal state but don't show
|
||||||
|
// If data update, we might need to update OUR instance?
|
||||||
|
// But socket hooks call render() on ui.countdowns.
|
||||||
|
// We should notify Movable if it's open.
|
||||||
|
|
||||||
|
const movable = Object.values(ui.windows).find(w => w instanceof MovableCountdowns);
|
||||||
|
if (movable) movable.render();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return originalRender(options, ...args);
|
||||||
|
};
|
||||||
|
ui.countdowns._patched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
styles/module.css
Normal file
24
styles/module.css
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* Movable Countdowns Styling */
|
||||||
|
|
||||||
|
/* Override system positioning constraints */
|
||||||
|
.movable-countdowns.daggerheart.dh-style.countdowns {
|
||||||
|
position: fixed !important;
|
||||||
|
/* Managed by AppV2 */
|
||||||
|
/* Reset system specific "docked" positioning */
|
||||||
|
right: auto !important;
|
||||||
|
align-self: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
transition: none !important;
|
||||||
|
/* Disable system transition for sliding */
|
||||||
|
box-shadow: 0 0 10px #000;
|
||||||
|
/* Add shadow for popping out */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure window header is visible/draggable */
|
||||||
|
.movable-countdowns .window-header {
|
||||||
|
cursor: grab !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure background image works if it relied on parent classes */
|
||||||
|
/* System uses .theme-dark .daggerheart.dh-style.countdowns */
|
||||||
|
/* If the body has theme-dark, this should still work. */
|
||||||
Loading…
Add table
Add a link
Reference in a new issue