Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbb4bfd860 | ||
|
|
52d700df3d |
@@ -231,7 +231,13 @@ below. Nothing is installed without someone clicking.
|
|||||||
Tile settings: channel icons, direction and timestamps.
|
Tile settings: channel icons, direction and timestamps.
|
||||||
- **ProPresenter** — what's live and what's next, with slide progress,
|
- **ProPresenter** — what's live and what's next, with slide progress,
|
||||||
driven by the shared `propresenter-core` client (ProPresenter 7/20).
|
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.
|
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,
|
- **Timers** — live ProPresenter timers (countdown, countdown-to-time,
|
||||||
elapsed) and LTC timecode as auto-sized cards. The + picker lists every
|
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"
|
discovered timer (and the LTC card) as its own tile, or add "All timers"
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
in /admin; the tile only renders. */
|
in /admin; the tile only renders. */
|
||||||
|
|
||||||
export default function create({ root, moduleApi }) {
|
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 state = null;
|
||||||
let feedOffline = false;
|
let feedOffline = false;
|
||||||
let stream = null;
|
let stream = null;
|
||||||
@@ -33,11 +39,13 @@ export default function create({ root, moduleApi }) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const screenEl = root.querySelector('.nn-screen');
|
const screenEl = root.querySelector('.nn-screen');
|
||||||
|
if (nowOnly) screenEl.classList.add('nn-only-now');
|
||||||
const topRow = root.querySelector('.nn-top');
|
const topRow = root.querySelector('.nn-top');
|
||||||
const playlistNameEl = root.querySelector('.nn-playlist');
|
const playlistNameEl = root.querySelector('.nn-playlist');
|
||||||
const liveEl = root.querySelector('.nn-live');
|
const liveEl = root.querySelector('.nn-live');
|
||||||
const liveTextEl = root.querySelector('.nn-live-text');
|
const liveTextEl = root.querySelector('.nn-live-text');
|
||||||
const nowEl = root.querySelector('.nn-now');
|
const nowEl = root.querySelector('.nn-now');
|
||||||
|
const nowLabelEl = root.querySelector('.nn-label');
|
||||||
const nowTitleBox = root.querySelector('.nn-title-box');
|
const nowTitleBox = root.querySelector('.nn-title-box');
|
||||||
let nowTitleEl = root.querySelector('.nn-title');
|
let nowTitleEl = root.querySelector('.nn-title');
|
||||||
const nowMetaEl = root.querySelector('.nn-meta');
|
const nowMetaEl = root.querySelector('.nn-meta');
|
||||||
@@ -47,9 +55,11 @@ export default function create({ root, moduleApi }) {
|
|||||||
const nextListEl = root.querySelector('.nn-next-list');
|
const nextListEl = root.querySelector('.nn-next-list');
|
||||||
const nextMoreEl = root.querySelector('.nn-next-more');
|
const nextMoreEl = root.querySelector('.nn-next-more');
|
||||||
|
|
||||||
|
/** A setting, defaulting to on — a Now tile's schema omits the Next ones. */
|
||||||
function prefs() {
|
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) ─ */
|
/* ── title tidying (same rule as propresenter-core.cleanItemName) ─ */
|
||||||
|
|
||||||
@@ -65,7 +75,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function displayName(raw) {
|
function displayName(raw) {
|
||||||
return prefs().cleanTitles ? cleanItemName(raw) : String(raw || '').trim();
|
return on('cleanTitles') ? cleanItemName(raw) : String(raw || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemTypeTag(type) {
|
function itemTypeTag(type) {
|
||||||
@@ -307,7 +317,9 @@ export default function create({ root, moduleApi }) {
|
|||||||
|
|
||||||
const total = state.currentSlideTotal || activeItem?.slideCount || 0;
|
const total = state.currentSlideTotal || activeItem?.slideCount || 0;
|
||||||
const current = state.currentSlide || 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);
|
const remaining = Math.max(0, total - current);
|
||||||
nowMetaEl.textContent = `Slide ${current} / ${total} · ${remaining} left`;
|
nowMetaEl.textContent = `Slide ${current} / ${total} · ${remaining} left`;
|
||||||
} else if (total > 0) {
|
} else if (total > 0) {
|
||||||
@@ -318,7 +330,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
nowMetaEl.textContent = '';
|
nowMetaEl.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs().showProgress && total > 0) {
|
if (on('showProgress') && total > 0) {
|
||||||
progressEl.hidden = false;
|
progressEl.hidden = false;
|
||||||
const pct = current > 0 ? Math.max(0, Math.min(100, (current / total) * 100)) : 0;
|
const pct = current > 0 ? Math.max(0, Math.min(100, (current / total) * 100)) : 0;
|
||||||
progressFillEl.style.width = `${pct}%`;
|
progressFillEl.style.width = `${pct}%`;
|
||||||
@@ -349,6 +361,10 @@ export default function create({ root, moduleApi }) {
|
|||||||
nameEl.textContent = displayName(item.name) || '(untitled)';
|
nameEl.textContent = displayName(item.name) || '(untitled)';
|
||||||
row.appendChild(nameEl);
|
row.appendChild(nameEl);
|
||||||
|
|
||||||
|
if (!on('showTags')) {
|
||||||
|
nextListEl.appendChild(row);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (item.type === 'presentation' && Number.isInteger(item.slideCount)) {
|
if (item.type === 'presentation' && Number.isInteger(item.slideCount)) {
|
||||||
const chip = document.createElement('div');
|
const chip = document.createElement('div');
|
||||||
chip.className = 'nn-count-chip';
|
chip.className = 'nn-count-chip';
|
||||||
@@ -366,7 +382,7 @@ export default function create({ root, moduleApi }) {
|
|||||||
nextListEl.appendChild(row);
|
nextListEl.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextLabelEl.hidden = upcoming.length === 0;
|
nextLabelEl.hidden = upcoming.length === 0 || !on('showNextLabel');
|
||||||
nextMoreEl.hidden = true;
|
nextMoreEl.hidden = true;
|
||||||
markNextOverflow();
|
markNextOverflow();
|
||||||
requestAnimationFrame(markNextOverflow);
|
requestAnimationFrame(markNextOverflow);
|
||||||
@@ -374,12 +390,22 @@ export default function create({ root, moduleApi }) {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
screenEl.classList.toggle('is-offline', feedOffline);
|
screenEl.classList.toggle('is-offline', feedOffline);
|
||||||
const showTop = prefs().showPlaylist || feedOffline || !state?.reachable || !state?.enabled;
|
// Trouble always shows, whatever is switched off: a tile that has stopped
|
||||||
topRow.classList.toggle('hidden-row', !showTop);
|
// updating must never look like one with nothing live.
|
||||||
playlistNameEl.textContent = prefs().showPlaylist ? state?.playlistName || '' : '';
|
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();
|
renderLive();
|
||||||
renderNow();
|
renderNow();
|
||||||
renderNext();
|
if (!nowOnly && on('showNext')) renderNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── sizing: --unit scales the whole card to the tile ───────────── */
|
/* ── sizing: --unit scales the whole card to the tile ───────────── */
|
||||||
@@ -387,6 +413,10 @@ export default function create({ root, moduleApi }) {
|
|||||||
function applyUnit() {
|
function applyUnit() {
|
||||||
const unit = Math.max(6.5, Math.min(20, root.clientHeight * 0.032));
|
const unit = Math.max(6.5, Math.min(20, root.clientHeight * 0.032));
|
||||||
screenEl.style.setProperty('--unit', unit.toFixed(2) + 'px');
|
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 ──────────────────────────────────────────────────── */
|
/* ── live feed ──────────────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"id": "propresenter-now-next",
|
"id": "propresenter-now-next",
|
||||||
"name": "ProPresenter",
|
"name": "ProPresenter",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"proddash": ">=1.1.0",
|
"proddash": ">=1.5.0",
|
||||||
"description": "What is live in ProPresenter and what comes next",
|
"description": "What is live in ProPresenter and what comes next",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{ "kind": "local-network", "reason": "Reaching ProPresenter on this network" }
|
{ "kind": "local-network", "reason": "Reaching ProPresenter on this network" }
|
||||||
@@ -20,13 +20,22 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"label": "API password (only if set in ProPresenter)",
|
"label": "Password",
|
||||||
"default": ""
|
"default": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"instanceSchema": {
|
"instanceSchema": {
|
||||||
"showPlaylist": { "type": "boolean", "label": "Show the playlist name", "default": true },
|
"cleanTitles": { "type": "switch", "label": "Tidy titles", "default": true },
|
||||||
"showProgress": { "type": "boolean", "label": "Show the slide progress bar", "default": true },
|
"showPlaylist": { "type": "switch", "label": "Playlist", "default": true, "group": "Show" },
|
||||||
"cleanTitles": { "type": "boolean", "label": "Tidy titles (hide [ Full ] tags and date codes)", "default": true }
|
"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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
routes() {
|
||||||
return {
|
return {
|
||||||
'GET /state': (req, res) => {
|
'GET /state': (req, res) => {
|
||||||
|
|||||||
@@ -21,6 +21,51 @@
|
|||||||
justify-content: center;
|
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 ── */
|
/* ── top row ── */
|
||||||
|
|
||||||
[data-module="propresenter-now-next"] .nn-top {
|
[data-module="propresenter-now-next"] .nn-top {
|
||||||
@@ -59,6 +104,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
[data-module="propresenter-now-next"] .nn-playlist[hidden] { display: none; }
|
||||||
|
|
||||||
[data-module="propresenter-now-next"] .nn-live {
|
[data-module="propresenter-now-next"] .nn-live {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -71,6 +117,7 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
[data-module="propresenter-now-next"] .nn-live[hidden] { display: none; }
|
||||||
|
|
||||||
[data-module="propresenter-now-next"] .nn-live-dot {
|
[data-module="propresenter-now-next"] .nn-live-dot {
|
||||||
width: calc(var(--unit) * 0.6);
|
width: calc(var(--unit) * 0.6);
|
||||||
@@ -122,6 +169,7 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
[data-module="propresenter-now-next"] .nn-label[hidden] { display: none; }
|
||||||
|
|
||||||
[data-module="propresenter-now-next"] .nn-title-box {
|
[data-module="propresenter-now-next"] .nn-title-box {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user