From 52d700df3d95bbfb9e4b3ce25861b13b8c0db180 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Wed, 16 Sep 2026 19:39:55 -0400 Subject: [PATCH] ProPresenter 1.1.0: a Now tile, and switches for everything on it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The + picker now offers the module two ways. "Now / Next" is the card as it was. "Now" is the live item on its own: the gradient comes off the card and fills the tile edge to edge, the border, radius and box go, and the playlist name and live dot float over the colour instead of sitting above it. A wall of them reads as panels of colour rather than cards on a background. Every part of the tile is now a switch in a "Show" group in the gear — playlist, live status, the NOW and NEXT labels, slide count, progress bar, the next list and its item tags. One or two words each, no descriptions. The Now entry carries its own six, since the three Next ones have nothing to act on there. Two things the tile still insists on. Trouble overrides the switches: the top row and the live chip come back the moment the feed drops, whatever is switched off, because a tile that has stopped updating must never look like one with nothing live. And at the Now tile's shortest sizes the floating top row would sit on the title, so below the height it needs it goes — the header dot and the title itself still say what is wrong. Co-Authored-By: Claude Opus 5 --- README.md | 6 +++ modules/propresenter-now-next/client.js | 48 ++++++++++++++++++----- modules/propresenter-now-next/module.json | 21 +++++++--- modules/propresenter-now-next/server.js | 37 +++++++++++++++++ modules/propresenter-now-next/style.css | 48 +++++++++++++++++++++++ 5 files changed, 145 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0a9dbc3..92b2feb 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,13 @@ below. Nothing is installed without someone clicking. Tile settings: channel icons, direction and timestamps. - **ProPresenter** — what's live and what's next, with slide progress, driven by the shared `propresenter-core` client (ProPresenter 7/20). + The + picker offers two tiles: **Now / Next**, the full card, and **Now**, + the live item alone with the gradient filling the tile edge to edge — a + wall panel that says one thing. Admin config: ProPresenter endpoint (host : port), password. + Tile settings: a **Show** group of switches for the playlist name, live + status, NOW and NEXT labels, slide count, progress bar, the next list and + its item tags, plus tidy titles. - **Timers** — live ProPresenter timers (countdown, countdown-to-time, elapsed) and LTC timecode as auto-sized cards. The + picker lists every discovered timer (and the LTC card) as its own tile, or add "All timers" diff --git a/modules/propresenter-now-next/client.js b/modules/propresenter-now-next/client.js index 624cca9..f168a88 100644 --- a/modules/propresenter-now-next/client.js +++ b/modules/propresenter-now-next/client.js @@ -6,6 +6,12 @@ in /admin; the tile only renders. */ export default function create({ root, moduleApi }) { + /* "now" is the live item on its own, filling the tile; "full" is that plus + the rest of the playlist. The picker presets `view`; a tile placed before + this module had a picker has neither, and gets the full card. */ + const view = String(moduleApi.instanceSettings.view || moduleApi.variant || 'full'); + const nowOnly = view === 'now'; + let state = null; let feedOffline = false; let stream = null; @@ -33,11 +39,13 @@ export default function create({ root, moduleApi }) { `; const screenEl = root.querySelector('.nn-screen'); + if (nowOnly) screenEl.classList.add('nn-only-now'); const topRow = root.querySelector('.nn-top'); const playlistNameEl = root.querySelector('.nn-playlist'); const liveEl = root.querySelector('.nn-live'); const liveTextEl = root.querySelector('.nn-live-text'); const nowEl = root.querySelector('.nn-now'); + const nowLabelEl = root.querySelector('.nn-label'); const nowTitleBox = root.querySelector('.nn-title-box'); let nowTitleEl = root.querySelector('.nn-title'); const nowMetaEl = root.querySelector('.nn-meta'); @@ -47,9 +55,11 @@ export default function create({ root, moduleApi }) { const nextListEl = root.querySelector('.nn-next-list'); const nextMoreEl = root.querySelector('.nn-next-more'); + /** A setting, defaulting to on — a Now tile's schema omits the Next ones. */ function prefs() { - return moduleApi.instanceSettings; // showPlaylist / showProgress / cleanTitles + return moduleApi.instanceSettings; } + const on = (key) => prefs()[key] !== false; /* ── title tidying (same rule as propresenter-core.cleanItemName) ─ */ @@ -65,7 +75,7 @@ export default function create({ root, moduleApi }) { } function displayName(raw) { - return prefs().cleanTitles ? cleanItemName(raw) : String(raw || '').trim(); + return on('cleanTitles') ? cleanItemName(raw) : String(raw || '').trim(); } function itemTypeTag(type) { @@ -307,7 +317,9 @@ export default function create({ root, moduleApi }) { const total = state.currentSlideTotal || activeItem?.slideCount || 0; const current = state.currentSlide || 0; - if (total > 0 && current > 0) { + if (!on('showSlideCount')) { + nowMetaEl.textContent = ''; + } else if (total > 0 && current > 0) { const remaining = Math.max(0, total - current); nowMetaEl.textContent = `Slide ${current} / ${total} · ${remaining} left`; } else if (total > 0) { @@ -318,7 +330,7 @@ export default function create({ root, moduleApi }) { nowMetaEl.textContent = ''; } - if (prefs().showProgress && total > 0) { + if (on('showProgress') && total > 0) { progressEl.hidden = false; const pct = current > 0 ? Math.max(0, Math.min(100, (current / total) * 100)) : 0; progressFillEl.style.width = `${pct}%`; @@ -349,6 +361,10 @@ export default function create({ root, moduleApi }) { nameEl.textContent = displayName(item.name) || '(untitled)'; row.appendChild(nameEl); + if (!on('showTags')) { + nextListEl.appendChild(row); + continue; + } if (item.type === 'presentation' && Number.isInteger(item.slideCount)) { const chip = document.createElement('div'); chip.className = 'nn-count-chip'; @@ -366,7 +382,7 @@ export default function create({ root, moduleApi }) { nextListEl.appendChild(row); } - nextLabelEl.hidden = upcoming.length === 0; + nextLabelEl.hidden = upcoming.length === 0 || !on('showNextLabel'); nextMoreEl.hidden = true; markNextOverflow(); requestAnimationFrame(markNextOverflow); @@ -374,12 +390,22 @@ export default function create({ root, moduleApi }) { function render() { screenEl.classList.toggle('is-offline', feedOffline); - const showTop = prefs().showPlaylist || feedOffline || !state?.reachable || !state?.enabled; - topRow.classList.toggle('hidden-row', !showTop); - playlistNameEl.textContent = prefs().showPlaylist ? state?.playlistName || '' : ''; + // Trouble always shows, whatever is switched off: a tile that has stopped + // updating must never look like one with nothing live. + const trouble = feedOffline || !state?.reachable || !state?.enabled; + const wantPlaylist = on('showPlaylist'); + const wantLive = on('showLive') || trouble; + topRow.classList.toggle('hidden-row', !wantPlaylist && !wantLive); + liveEl.hidden = !wantLive; + // Hidden, not just emptied: it is flex:1, so an emptied one still holds + // the row open and pushes the live dot off the right edge. + playlistNameEl.hidden = !wantPlaylist; + playlistNameEl.textContent = state?.playlistName || ''; + nowLabelEl.hidden = !on('showNowLabel'); + screenEl.classList.toggle('nn-no-next', nowOnly || !on('showNext')); renderLive(); renderNow(); - renderNext(); + if (!nowOnly && on('showNext')) renderNext(); } /* ── sizing: --unit scales the whole card to the tile ───────────── */ @@ -387,6 +413,10 @@ export default function create({ root, moduleApi }) { function applyUnit() { const unit = Math.max(6.5, Math.min(20, root.clientHeight * 0.032)); screenEl.style.setProperty('--unit', unit.toFixed(2) + 'px'); + // On a Now tile the top row floats over the gradient, so at the shortest + // sizes it would sit on the title. Below the height it needs, it goes — + // the header dot and the title itself still report trouble. + screenEl.classList.toggle('nn-tight', nowOnly && root.clientHeight < unit * 9); } /* ── live feed ──────────────────────────────────────────────────── */ diff --git a/modules/propresenter-now-next/module.json b/modules/propresenter-now-next/module.json index fe25d0d..67d188e 100644 --- a/modules/propresenter-now-next/module.json +++ b/modules/propresenter-now-next/module.json @@ -1,8 +1,8 @@ { "id": "propresenter-now-next", "name": "ProPresenter", - "version": "1.0.1", - "proddash": ">=1.1.0", + "version": "1.1.0", + "proddash": ">=1.5.0", "description": "What is live in ProPresenter and what comes next", "permissions": [ { "kind": "local-network", "reason": "Reaching ProPresenter on this network" } @@ -20,13 +20,22 @@ }, "password": { "type": "password", - "label": "API password (only if set in ProPresenter)", + "label": "Password", "default": "" } }, "instanceSchema": { - "showPlaylist": { "type": "boolean", "label": "Show the playlist name", "default": true }, - "showProgress": { "type": "boolean", "label": "Show the slide progress bar", "default": true }, - "cleanTitles": { "type": "boolean", "label": "Tidy titles (hide [ Full ] tags and date codes)", "default": true } + "cleanTitles": { "type": "switch", "label": "Tidy titles", "default": true }, + "showPlaylist": { "type": "switch", "label": "Playlist", "default": true, "group": "Show" }, + "showLive": { "type": "switch", "label": "Live status", "default": true, "group": "Show" }, + "showNowLabel": { "type": "switch", "label": "Now label", "default": true, "group": "Show" }, + "showSlideCount": { "type": "switch", "label": "Slide count", "default": true, "group": "Show" }, + "showProgress": { "type": "switch", "label": "Progress bar", "default": true, "group": "Show" }, + "showNext": { "type": "switch", "label": "Next list", "default": true, "group": "Show" }, + "showNextLabel": { "type": "switch", "label": "Next label", "default": true, "group": "Show" }, + "showTags": { "type": "switch", "label": "Item tags", "default": true, "group": "Show" } + }, + "instanceGroups": { + "Show": { "collapsed": false, "columns": 2 } } } diff --git a/modules/propresenter-now-next/server.js b/modules/propresenter-now-next/server.js index 781cccb..e13503e 100644 --- a/modules/propresenter-now-next/server.js +++ b/modules/propresenter-now-next/server.js @@ -108,6 +108,43 @@ module.exports = { }; }, + /** + * Two ways to place this module. "Now / Next" is the full card — what is + * live, then the rest of the playlist. "Now" is the same live item alone, + * edge to edge with the gradient filling the tile: a wall display that says + * one thing. The Now entry offers only the settings it can act on, so its + * gear carries no Next switches. + */ + tiles() { + return [ + { + id: 'full', + name: 'Now / Next', + description: 'What is live, and the rest of the playlist', + defaultSize: { w: 4, h: 5 }, + minSize: { w: 3, h: 3 }, + settings: { view: 'full' }, + }, + { + id: 'now', + name: 'Now', + description: 'Only what is live, filling the tile', + defaultSize: { w: 4, h: 2 }, + minSize: { w: 2, h: 1 }, + settings: { view: 'now' }, + instanceSchema: { + cleanTitles: { type: 'switch', label: 'Tidy titles', default: true }, + showPlaylist: { type: 'switch', label: 'Playlist', default: true, group: 'Show' }, + showLive: { type: 'switch', label: 'Live status', default: true, group: 'Show' }, + showNowLabel: { type: 'switch', label: 'Now label', default: true, group: 'Show' }, + showSlideCount: { type: 'switch', label: 'Slide count', default: true, group: 'Show' }, + showProgress: { type: 'switch', label: 'Progress bar', default: true, group: 'Show' }, + }, + instanceGroups: { Show: { collapsed: false, columns: 2 } }, + }, + ]; + }, + routes() { return { 'GET /state': (req, res) => { diff --git a/modules/propresenter-now-next/style.css b/modules/propresenter-now-next/style.css index 8ef2efc..3962d09 100644 --- a/modules/propresenter-now-next/style.css +++ b/modules/propresenter-now-next/style.css @@ -21,6 +21,51 @@ justify-content: center; } +/* NEXT switched off, or a Now tile: the list goes and the card takes the room. */ +[data-module="propresenter-now-next"] .nn-screen.nn-no-next .nn-next-wrap { display: none; } +[data-module="propresenter-now-next"] .nn-screen.nn-no-next { justify-content: center; } + +/* ── the "Now" tile ── + Only what is live, edge to edge: the gradient moves off the card and onto + the whole tile, the card loses its border, radius and box, and the title + sits in the middle of everything. Nothing is inset, so a wall of these + reads as panels of colour rather than cards on a background. */ +[data-module="propresenter-now-next"] .nn-screen.nn-only-now { + gap: 0; + background: linear-gradient( + 160deg, + color-mix(in srgb, var(--accent) 14%, transparent), + color-mix(in srgb, var(--bg) 70%, transparent) + ); +} +[data-module="propresenter-now-next"] .nn-only-now .nn-now { + flex: 1; + height: auto; + min-height: 0; + justify-content: center; + border: none; + border-radius: 0; + background: none; + padding: 0; +} +/* The live dot and playlist ride over the gradient rather than above it. */ +[data-module="propresenter-now-next"] .nn-only-now .nn-top { + position: absolute; + inset: var(--nn-pad) var(--nn-pad) auto; + pointer-events: none; +} +[data-module="propresenter-now-next"] .nn-only-now .nn-title-box { margin-top: 0; } +/* The overlay floats, so it takes no room — this keeps the title clear of it + and still centred, by holding back as much at the bottom as at the top. */ +[data-module="propresenter-now-next"] .nn-only-now:not(.nn-tight) .nn-now { + padding: calc(var(--unit) * 1.4) 0; +} +/* Too short for the overlay: the title gets the whole panel to itself. */ +[data-module="propresenter-now-next"] .nn-only-now.nn-tight .nn-top, +[data-module="propresenter-now-next"] .nn-only-now.nn-tight.is-offline .nn-top { + display: none !important; +} + /* ── top row ── */ [data-module="propresenter-now-next"] .nn-top { @@ -59,6 +104,7 @@ overflow: hidden; text-overflow: ellipsis; } +[data-module="propresenter-now-next"] .nn-playlist[hidden] { display: none; } [data-module="propresenter-now-next"] .nn-live { display: flex; @@ -71,6 +117,7 @@ text-transform: uppercase; color: var(--muted); } +[data-module="propresenter-now-next"] .nn-live[hidden] { display: none; } [data-module="propresenter-now-next"] .nn-live-dot { width: calc(var(--unit) * 0.6); @@ -122,6 +169,7 @@ text-transform: uppercase; color: var(--accent); } +[data-module="propresenter-now-next"] .nn-label[hidden] { display: none; } [data-module="propresenter-now-next"] .nn-title-box { flex: 1;