Skip to content

Commit 87816d7

Browse files
authored
Merge pull request #23 from paulcheeba/v13.0.8.1.0
V13.0.8.1.0
2 parents 726493f + e211c3a commit 87816d7

File tree

6 files changed

+391
-21
lines changed

6 files changed

+391
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# About Time (v13.0.7.7)
1+
# About Time (v13.0.8.1.0)
22

33

44
**About Time** is a timekeeping and event scheduling utility for Foundry VTT v13+.

about-time.js

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -178,23 +178,7 @@ Hooks.once('ready', () => {
178178
return `${sign}${pad(dd)}:${pad(hh)}:${pad(mm)}:${pad(ss)}`;
179179
};
180180
}
181-
182-
Hooks.on("chatMessage", (_log, content) => {
183-
try {
184-
if (typeof content !== "string") return false;
185-
const msg = content.trim();
186-
if (!/^\/at\s+list\b/i.test(msg)) return false;
187-
188-
if (typeof api.chatQueue === "function") {
189-
api.chatQueue({});
190-
return true;
191-
}
192-
return false;
193-
} catch (e) {
194-
console.error("[About Time] '/at list' alias failed:", e);
195-
return false;
196-
}
197-
});
181+
// Removed in v13.0.8-dev: redundant "/at list" alias lived here and was breaking normal chat.
198182
} catch (err) {
199183
console.error("[About Time] v13.0.6.1 helpers failed to initialize:", err);
200184
}

macros/devTestMacros.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Macro (Script) to open the new AppV2 window (dev-only)
2+
(async () => {
3+
const modPath = "/modules/about-time-v13/module/ATEventManagerAppV2.js";
4+
const { ATEventManagerAppV2 } = await import(modPath);
5+
new ATEventManagerAppV2().render(true);
6+
})();
7+
8+
//

module.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"id": "about-time-v13",
33
"title": "About Time",
44
"description": "Time and scheduling utility for Foundry VTT, updated for v13 compatibility.",
5-
"version": "13.0.7.7",
5+
"version": "13.0.8.0",
66
"compatibility": {
7-
"minimum": "11",
7+
"minimum": "12",
88
"verified": "13",
99
"maximum": "13.999"
1010
},
@@ -13,7 +13,7 @@
1313
"name": "tposney"
1414
},
1515
{
16-
"name": "crusherDestroyer666"
16+
"name": "crusherDestroyer666/paulcheeba"
1717
},
1818
{
1919
"name": "chatGPT"

module/ATEventManagerAppV2.js

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// File: modules/about-time-v13/module/ATEventManagerAppV2.js
2+
// v13.0.8.1.0 — Starts fallback "in DD:HH:MM:SS"; window width 920px; row Stop wired.
3+
// NOTE: Copy UID action remains defined (harmless), but the button was removed from the template.
4+
5+
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; // v12+
6+
import { ElapsedTime } from "./ElapsedTime.js";
7+
import { MODULE_ID } from "./settings.js";
8+
9+
export class ATEventManagerAppV2 extends HandlebarsApplicationMixin(ApplicationV2) {
10+
static DEFAULT_OPTIONS = {
11+
id: "at-em-v2",
12+
classes: ["about-time", "at-emv2", "at-dracula"],
13+
tag: "form",
14+
window: { title: "About Time — Event Manager V2", icon: "fas fa-clock", resizable: true },
15+
position: { width: 920, height: "auto" },
16+
actions: {
17+
create(ev, el) { return this.onCreate(ev); },
18+
list(ev, el) { return this.onList(ev); },
19+
flush(ev, el) { return this.onFlush(ev); },
20+
"flush-rem"(ev, el) { return this.onFlushRem(ev); },
21+
"stop-by-name"(ev) { return this.onStopByName(ev); },
22+
"stop-by-uid"(ev) { return this.onStopByUID(ev); },
23+
"row-stop"(ev, el) { return this.onRowStop(ev, el); },
24+
"copy-uid"(ev, el) { return this.onCopyUID(ev, el); }
25+
}
26+
};
27+
28+
static PARTS = { body: { template: "modules/about-time-v13/templates/ATEventManagerAppV2.hbs" } };
29+
30+
#ticker = null;
31+
32+
async render(force, options = {}) {
33+
const out = await super.render(force, options);
34+
if (!this.#ticker) this.#startTicker();
35+
return out;
36+
}
37+
38+
async close(options) {
39+
this.#stopTicker();
40+
return super.close(options);
41+
}
42+
43+
async _prepareContext() {
44+
const now = game.time.worldTime;
45+
const q = ElapsedTime?._eventQueue;
46+
const entries = [];
47+
48+
if (q?.array && Number.isInteger(q.size)) {
49+
for (let i = 0; i < q.size; i++) {
50+
const e = q.array[i];
51+
if (!e) continue;
52+
const meta = e?._args?.[0] ?? {};
53+
const name = String(meta.__atName ?? "");
54+
const msg = String(meta.__atMsg ?? "");
55+
const inc = Number(e?._increment || 0);
56+
const time = Number(e._time || 0);
57+
entries.push({
58+
uid: e._uid,
59+
name,
60+
msg,
61+
time,
62+
startTxt: this.#fmtTimestamp(time),
63+
remainingTxt: this.#fmtDHMS(Math.max(0, Math.floor(time - now))),
64+
recurring: !!e?._recurring,
65+
incTxt: inc ? this.#fmtDHMS(inc) : ""
66+
});
67+
}
68+
}
69+
70+
return { isGM: !!game.user?.isGM, entries };
71+
}
72+
73+
// ---- Actions -------------------------------------------------------------
74+
async onCreate(event) {
75+
if (!game.user?.isGM) return ui.notifications?.warn?.("GM only");
76+
const fd = new FormData(this.form);
77+
const name = String(fd.get("eventName") || "").trim();
78+
const durStr = String(fd.get("duration") || "").trim();
79+
const message = String(fd.get("message") || "");
80+
const repeat = fd.get("repeat") === "on";
81+
const runMacro = fd.get("runMacro") === "on";
82+
const macroName = String(fd.get("macroName") || "").trim();
83+
84+
const seconds = this.#parseMixedDuration(durStr);
85+
if (!seconds || seconds <= 0) return this.#gmWhisper(`<p>[${MODULE_ID}] Enter a valid duration.</p>`);
86+
87+
const meta = { __atName: name || (runMacro ? macroName : "(unnamed)"), __atMsg: message };
88+
89+
const handler = async (metaArg) => {
90+
try {
91+
if (runMacro && macroName) {
92+
const macro = game.macros.getName?.(macroName) ?? game.macros.find?.(m => m.name === macroName);
93+
if (macro) {
94+
if (isNewerVersion(game.version, "11.0")) await macro.execute({ args: [metaArg] });
95+
else {
96+
const body = `return (async () => { ${macro.command} })()`;
97+
const fn = Function("{speaker, actor, token, character, item, args}={}", body);
98+
await fn.call(this, { speaker: {}, args: [metaArg] });
99+
}
100+
} else ui.notifications?.warn?.(`[${MODULE_ID}] Macro not found: ${macroName}`);
101+
} else {
102+
await this.#gmWhisper(`<p>[${MODULE_ID}] ${foundry.utils.escapeHTML(metaArg.__atMsg || metaArg.__atName || "(event)")}</p>`);
103+
}
104+
} catch (err) {
105+
console.error(`${MODULE_ID} | handler failed`, err);
106+
await this.#gmWhisper(`<p>[${MODULE_ID}] Handler error: ${foundry.utils.escapeHTML(err?.message || err)}</p>`);
107+
}
108+
};
109+
110+
const AT = game.abouttime ?? game.Gametime;
111+
const uid = repeat ? AT.doEvery({ seconds }, handler, meta) : AT.doIn({ seconds }, handler, meta);
112+
if (name) await game.user.setFlag(MODULE_ID, name, uid);
113+
114+
await this.#gmWhisper(
115+
`<p>[${MODULE_ID}] Created <strong>${repeat ? "repeating" : "one-time"}</strong> event:
116+
<code>${foundry.utils.escapeHTML(uid)}</code> — ${this.#fmtDHMS(seconds)} — “${foundry.utils.escapeHTML(meta.__atName)}”</p>`
117+
);
118+
this.render(true);
119+
}
120+
121+
async onStopByName(event) {
122+
if (!game.user?.isGM) return;
123+
const fd = new FormData(this.form);
124+
const key = String(fd.get("stopKey") || "").trim();
125+
if (!key) return this.#gmWhisper(`<p>[${MODULE_ID}] Enter an Event Name to stop.</p>`);
126+
127+
const AT = game.abouttime ?? game.Gametime;
128+
const q = ElapsedTime?._eventQueue;
129+
let count = 0;
130+
if (q?.array && Number.isInteger(q.size)) {
131+
const target = key.toLowerCase();
132+
for (let i = 0; i < q.size; i++) {
133+
const e = q.array[i];
134+
if ((e?._args?.[0]?.__atName || "").toLowerCase() === target) {
135+
if (AT.clearTimeout(e._uid)) count++;
136+
}
137+
}
138+
}
139+
140+
if (count) {
141+
const flags = (await game.user.getFlag(MODULE_ID)) || {};
142+
for (const k of Object.keys(flags)) if (flags[k] && typeof flags[k] === "string") {
143+
let exists = false;
144+
if (q?.array && Number.isInteger(q.size)) {
145+
for (let i = 0; i < q.size; i++) if (q.array[i]?._uid === flags[k]) { exists = true; break; }
146+
}
147+
if (!exists) await game.user.unsetFlag(MODULE_ID, k);
148+
}
149+
await this.#gmWhisper(`<p>[${MODULE_ID}] Stopped ${count} event(s) named <strong>${foundry.utils.escapeHTML(key)}</strong>.</p>`);
150+
} else {
151+
await this.#gmWhisper(`<p>[${MODULE_ID}] No events found named <strong>${foundry.utils.escapeHTML(key)}</strong>.</p>`);
152+
}
153+
this.render();
154+
}
155+
156+
async onStopByUID(event) {
157+
if (!game.user?.isGM) return;
158+
const fd = new FormData(this.form);
159+
const uid = String(fd.get("stopKey") || "").trim();
160+
if (!uid) return this.#gmWhisper(`<p>[${MODULE_ID}] Enter a UID to stop.</p>`);
161+
const ok = (game.abouttime ?? game.Gametime).clearTimeout(uid);
162+
if (ok) await this.#gmWhisper(`<p>[${MODULE_ID}] Stopped event <code>${foundry.utils.escapeHTML(uid)}</code>.</p>`);
163+
else await this.#gmWhisper(`<p>[${MODULE_ID}] No event found for UID <code>${foundry.utils.escapeHTML(uid)}</code>.</p>`);
164+
this.render();
165+
}
166+
167+
async onList() {
168+
(game.abouttime ?? game.Gametime).chatQueue({ showArgs: false, showUid: true, showDate: true, gmOnly: true });
169+
await this.#gmWhisper(`<p>[${MODULE_ID}] Queue listed to GM chat.</p>`);
170+
}
171+
172+
async onFlush() {
173+
const AT = game.abouttime ?? game.Gametime;
174+
const q = ElapsedTime?._eventQueue; const count = q?.size ?? 0;
175+
AT.flushQueue?.();
176+
await this.#gmWhisper(`<p>[${MODULE_ID}] Flushed ${count} event(s).</p>`);
177+
this.render();
178+
}
179+
180+
async onFlushRem() {
181+
const AT = game.abouttime ?? game.Gametime;
182+
const q = ElapsedTime?._eventQueue; const count = q?.size ?? 0;
183+
AT.flushQueue?.();
184+
AT.reminderIn?.({ seconds: 3600 }, `[${MODULE_ID}] Queue was flushed an hour ago.`);
185+
await this.#gmWhisper(`<p>[${MODULE_ID}] Flushed ${count} event(s) and scheduled 1h reminder.</p>`);
186+
this.render();
187+
}
188+
189+
async onRowStop(event, el) {
190+
if (!game.user?.isGM) return;
191+
const uid = el?.dataset?.uid || event?.currentTarget?.dataset?.uid;
192+
if (!uid) return;
193+
const ok = (game.abouttime ?? game.Gametime).clearTimeout(uid);
194+
if (ok) await this.#gmWhisper(`<p>[${MODULE_ID}] Stopped event <code>${foundry.utils.escapeHTML(uid)}</code>.</p>`);
195+
this.render();
196+
}
197+
198+
async onCopyUID(event, el) {
199+
const uid = el?.dataset?.uid || event?.currentTarget?.dataset?.uid;
200+
if (!uid) return;
201+
try {
202+
await navigator.clipboard?.writeText?.(uid);
203+
ui.notifications?.info?.("UID copied to clipboard");
204+
} catch {
205+
ui.notifications?.warn?.("Clipboard unavailable");
206+
}
207+
}
208+
209+
// ---- Helpers -------------------------------------------------------------
210+
#gmWhisper(html) {
211+
const ids = ChatMessage.getWhisperRecipients("GM").filter((u) => u.active).map((u) => u.id);
212+
return ChatMessage.create({ content: html, whisper: ids });
213+
}
214+
215+
#parseMixedDuration(input) {
216+
if (!input || typeof input !== "string") return 0;
217+
let total = 0; const re = /(\d+)\s*(d|day|days|h|hr|hrs|hour|hours|m|min|mins|minute|minutes|s|sec|secs|second|seconds)?/gi;
218+
let m; while ((m = re.exec(input))) {
219+
const v = Number(m[1]); const u = (m[2] || "s").toLowerCase();
220+
total += ["d","day","days"].includes(u) ? v*86400
221+
: ["h","hr","hrs","hour","hours"].includes(u) ? v*3600
222+
: ["m","min","mins","minute","minutes"].includes(u) ? v*60
223+
: v;
224+
}
225+
return Math.floor(total);
226+
}
227+
228+
#fmtDHMS(totalSeconds) {
229+
const s = Math.max(0, Math.floor(Number(totalSeconds) || 0));
230+
const d = Math.floor(s / 86400);
231+
const h = Math.floor((s % 86400) / 3600);
232+
const m = Math.floor((s % 3600) / 60);
233+
const sec = s % 60;
234+
const pad = (n) => String(n).padStart(2, "0");
235+
return `${String(d).padStart(2, "0")}:${pad(h)}:${pad(m)}:${pad(sec)}`;
236+
}
237+
238+
// If Simple Calendar is present, show its formatted date/time.
239+
// Otherwise, show a friendly relative start: "in DD:HH:MM:SS".
240+
#fmtTimestamp(ts) {
241+
const api = globalThis.SimpleCalendar?.api;
242+
if (api?.timestampToDate && api?.formatDateTime) {
243+
const dt = api.timestampToDate(ts);
244+
const f = api.formatDateTime(dt) ?? { date: "", time: `t+${ts}` };
245+
return `${f.date ? f.date + " " : ""}${f.time}`;
246+
}
247+
const now = game.time.worldTime ?? 0;
248+
const diff = Math.max(0, Math.floor(ts - now));
249+
return `in ${this.#fmtDHMS(diff)}`;
250+
}
251+
252+
#startTicker() {
253+
this.#stopTicker();
254+
this.#ticker = setInterval(() => {
255+
const now = game.time.worldTime;
256+
for (const el of this.element?.querySelectorAll?.("[data-remaining][data-time]") ?? []) {
257+
const time = Number(el.dataset.time || 0);
258+
el.textContent = this.#fmtDHMS(Math.max(0, Math.floor(time - now)));
259+
}
260+
}, 1000);
261+
}
262+
263+
#stopTicker() { if (this.#ticker) { clearInterval(this.#ticker); this.#ticker = null; } }
264+
}
265+
266+
// Convenience export for macro users
267+
export function openATEventManagerV2(options = {}) {
268+
return new ATEventManagerAppV2(options).render(true);
269+
}

0 commit comments

Comments
 (0)