Timers: present each timer and LTC in the Add-tile picker

Adopt main's new multi-tile contract: the module's server exports
tiles() (answered from the poller's held state, never a fresh upstream
request) listing All timers, one entry per discovered timer, and LTC
timecode. The client branches on moduleApi.variant — solo tiles render
exactly one card with no selection menu, and a solo timer that vanishes
from ProPresenter shows a labeled "Not in ProPresenter" card instead of
erroring (visible even in the compact layout). pp-timers-mock gains
--drop=<uuid,...> to simulate a timer deleted in ProPresenter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-08-31 14:34:59 -04:00
co-authored by Claude Fable 5
parent a8a382c171
commit 12245cba93
5 changed files with 90 additions and 8 deletions
+42 -2
View File
@@ -18,6 +18,17 @@ export default function create({ root, moduleApi }) {
let resizeObserver = null;
let hiddenItems = new Set(moduleApi.instanceSettings.hiddenItems || []);
// Which picker entry this tile was added as (the module's tiles() list):
// '' or 'all' = every item, selectable via the Timers ▾ menu;
// 'timer:<uuid>' or 'ltc' = exactly one card, no menu.
const variant = String(moduleApi.variant || '');
const soloKey = variant === 'ltc' ? LTC_KEY
: variant.startsWith('timer:') ? variant.slice('timer:'.length)
: '';
/** Last name seen for a solo timer, so its card stays labeled if the
timer is later deleted in ProPresenter. */
let lastSoloName = '';
/* ── DOM skeleton ───────────────────────────────────────────────── */
root.innerHTML = `
@@ -38,7 +49,7 @@ export default function create({ root, moduleApi }) {
/* ── title-bar menu: pick which timers / LTC this tile shows ────── */
let menuEl = null; // the open menu's element (filled on each open)
const itemsMenu = moduleApi.header.addMenu({
const itemsMenu = soloKey ? null : moduleApi.header.addMenu({
label: 'Timers ▾',
title: 'Choose which timers this tile shows',
build(menu) {
@@ -67,6 +78,7 @@ export default function create({ root, moduleApi }) {
}
function updateMenuLabel() {
if (!itemsMenu) return;
const rows = menuRows();
const visible = rows.filter((r) => !hiddenItems.has(r.key)).length;
itemsMenu.setLabel(visible === rows.length
@@ -131,11 +143,23 @@ export default function create({ root, moduleApi }) {
}
function visibleItems() {
const ltcState = state?.ltc || { supported: null, time: '', receiving: false };
if (soloKey === LTC_KEY) return [{ key: LTC_KEY, ltc: ltcState }];
if (soloKey) {
const timer = (state?.timers || []).find((t) => t.uuid === soloKey);
if (timer) {
lastSoloName = timer.name;
return [{ key: soloKey, timer }];
}
// The timer this tile was added for isn't in ProPresenter's list (yet,
// or any more) — keep a clearly-labeled card up rather than erroring.
return [{ key: soloKey, missing: true }];
}
const items = [];
for (const t of state?.timers || []) {
if (!hiddenItems.has(t.uuid)) items.push({ key: t.uuid, timer: t });
}
if (!hiddenItems.has(LTC_KEY)) items.push({ key: LTC_KEY, ltc: state?.ltc || { supported: null, time: '', receiving: false } });
if (!hiddenItems.has(LTC_KEY)) items.push({ key: LTC_KEY, ltc: ltcState });
return items;
}
@@ -158,6 +182,21 @@ export default function create({ root, moduleApi }) {
return { el, nameEl, timeEl, stateEl };
}
/** A solo tile whose timer vanished from ProPresenter's list. While the
server is unreachable the list is merely unknown — the banner already
says so, so only claim "not in ProPresenter" when actually connected. */
function updateMissingCard(card) {
card.nameEl.textContent = lastSoloName || 'Timer';
card.timeEl.textContent = '—';
if (state?.reachable) {
card.stateEl.textContent = 'Not in ProPresenter';
card.el.className = 'tm-card is-missing';
} else {
card.stateEl.textContent = '—';
card.el.className = 'tm-card is-stopped';
}
}
function updateTimerCard(card, timer) {
card.nameEl.textContent = timer.name;
card.timeEl.textContent = displayTime(timer.time);
@@ -254,6 +293,7 @@ export default function create({ root, moduleApi }) {
structureChanged = true;
}
if (item.ltc) updateLtcCard(card, item.ltc);
else if (item.missing) updateMissingCard(card);
else updateTimerCard(card, item.timer);
}
// Keep DOM order in step with the state's order (config-list order).
+29
View File
@@ -314,6 +314,35 @@ module.exports = {
};
},
/**
* The Add-tile picker's entries: one per discovered timer, plus "All
* timers" and the LTC card (moduleApi.variant carries the entry id —
* 'all', 'timer:<uuid>' or 'ltc'). Answered from the poller's latest
* state, never from a fresh upstream request — the picker must stay
* instant, and a dead ProPresenter must not stall it.
*/
tiles() {
const state = current ? current.getLatest() : null;
if (!state || !state.enabled) return []; // unconfigured → classic single entry
const TYPE_LABEL = {
countdown: 'Countdown timer',
countdown_to_time: 'Countdown to a time of day',
elapsed: 'Elapsed timer',
unknown: 'Timer',
};
const oneCard = { defaultSize: { w: 3, h: 2 }, minSize: { w: 2, h: 1 } };
return [
{ id: 'all', name: 'All timers', description: 'Every timer and the LTC timecode, selectable per tile' },
...state.timers.map((t) => ({
id: `timer:${t.uuid}`,
name: t.name,
description: TYPE_LABEL[t.type] || 'Timer',
...oneCard,
})),
{ id: 'ltc', name: 'LTC timecode', description: 'Incoming timecode (HH:MM:SS:FF)', ...oneCard },
];
},
routes() {
return {
'GET /state': (req, res) => {
+6
View File
@@ -63,6 +63,7 @@
that message must never be hidden. */
[data-module="propresenter-timers"] .tm-compact .tm-card .tm-state { display: none; }
[data-module="propresenter-timers"] .tm-compact .tm-card-ltc.is-ltc-unavailable .tm-state { display: block; }
[data-module="propresenter-timers"] .tm-compact .tm-card.is-missing .tm-state { display: block; }
/* ── one timer card ── */
@@ -139,6 +140,11 @@
[data-module="propresenter-timers"] .tm-card.is-complete .tm-time { color: var(--warn); }
[data-module="propresenter-timers"] .tm-card.is-complete .tm-state { color: var(--warn); }
/* A solo tile whose timer was deleted in ProPresenter. */
[data-module="propresenter-timers"] .tm-card.is-missing { border-style: dashed; }
[data-module="propresenter-timers"] .tm-card.is-missing .tm-time { color: var(--muted); }
[data-module="propresenter-timers"] .tm-card.is-missing .tm-state { color: var(--warn); }
/* Overrun: readable across the room even on a wall display. */
[data-module="propresenter-timers"] .tm-card.is-overrun {
border-color: var(--danger);