mirror of
https://github.com/stagehacks/Cue-View.git
synced 2026-08-10 08:13:38 +00:00
Feature: Ontime (#373)
* Feature: Ontime support, WebSocket, Embedded fonts. * Fix: Clock matching operator view Also more notes showing * Fix: ws path+font assets
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<p>Connects to <strong>Ontime</strong> via WebSocket.</p>
|
||||
<p>Default port: <strong>4001</strong></p>
|
||||
<p>Displays the current timer, playback state, and event information from the active rundown.</p>
|
||||
@@ -0,0 +1,60 @@
|
||||
exports.config = {
|
||||
defaultName: 'Ontime',
|
||||
connectionType: 'websocket',
|
||||
remotePort: 4001,
|
||||
mayChangePorts: true,
|
||||
heartbeatInterval: 500,
|
||||
heartbeatTimeout: 15000,
|
||||
fields: [
|
||||
{
|
||||
key: 'wsPath',
|
||||
label: 'WS Path',
|
||||
type: 'textinput',
|
||||
value: '/ws',
|
||||
action: () => {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
exports.ready = function ready(device) {
|
||||
const d = device;
|
||||
d.data = {
|
||||
timer: { current: null, playback: 'stop', addedTime: 0, duration: null, elapsed: null },
|
||||
eventNow: null,
|
||||
eventNext: null,
|
||||
clock: null,
|
||||
};
|
||||
d.send({ tag: 'poll' });
|
||||
};
|
||||
|
||||
exports.data = function data(_device, msg) {
|
||||
const device = _device;
|
||||
|
||||
if (!device.data) {
|
||||
device.data = {
|
||||
timer: { current: null, playback: 'stop', addedTime: 0, duration: null, elapsed: null },
|
||||
eventNow: null,
|
||||
eventNext: null,
|
||||
clock: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Ontime v4 sends all state updates as { tag: "runtime-data", payload: Partial<RuntimeStore> }
|
||||
// Poll responses arrive as { tag: "poll", payload: Partial<RuntimeStore> }
|
||||
if (msg.tag !== 'runtime-data' && msg.tag !== 'poll') return;
|
||||
|
||||
const p = msg.payload;
|
||||
if (!p) return;
|
||||
if (p.timer !== undefined) device.data.timer = p.timer;
|
||||
if (p.eventNow !== undefined) device.data.eventNow = p.eventNow;
|
||||
if (p.eventNext !== undefined) device.data.eventNext = p.eventNext;
|
||||
if (p.clock !== undefined) device.data.clock = p.clock;
|
||||
|
||||
device.draw();
|
||||
};
|
||||
|
||||
exports.heartbeat = function heartbeat(device) {
|
||||
device.send({ tag: 'poll' });
|
||||
};
|
||||
|
||||
exports.update = function update(_device, _doc, _updateType, _data) {};
|
||||
@@ -0,0 +1,177 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(../../src/assets/font/OpenSans-Light.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(../../src/assets/font/OpenSans-Regular.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(../../src/assets/font/OpenSans-SemiBold.ttf) format('truetype');
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #161616;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
.ontime-top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 10px 14px;
|
||||
background-color: #222;
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
|
||||
.ontime-top-bar.play {
|
||||
border-bottom-color: #4caf50;
|
||||
}
|
||||
|
||||
.ontime-top-bar.pause {
|
||||
border-bottom-color: #ff9800;
|
||||
}
|
||||
|
||||
.ontime-top-bar.armed {
|
||||
border-bottom-color: #2196f3;
|
||||
}
|
||||
|
||||
.ontime-top-bar.roll {
|
||||
border-bottom-color: #9c27b0;
|
||||
}
|
||||
|
||||
.ontime-top-bar.overtime {
|
||||
border-bottom-color: #f44336;
|
||||
}
|
||||
|
||||
.ontime-top-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ontime-top-clock {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.ontime-top-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ontime-timer-value {
|
||||
font-size: 52px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.ontime-top-bar.overtime .ontime-timer-value {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.ontime-top-bar.stop .ontime-timer-value,
|
||||
.ontime-top-bar.armed .ontime-timer-value {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.ontime-clock-value {
|
||||
font-size: 52px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.ontime-progress-bar {
|
||||
height: 3px;
|
||||
background-color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ontime-progress-fill {
|
||||
height: 100%;
|
||||
background-color: #4caf50;
|
||||
transition: width 0.5s linear;
|
||||
}
|
||||
|
||||
.ontime-added-time {
|
||||
font-size: 12px;
|
||||
color: #ff9800;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.ontime-event {
|
||||
margin: 8px 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
background-color: #1e1e1e;
|
||||
border-left: 3px solid #444;
|
||||
}
|
||||
|
||||
.ontime-event-now {
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.ontime-event-next {
|
||||
border-left-color: #444;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.ontime-event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ontime-event-cue {
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.ontime-event-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ontime-event-title {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ontime-event-note {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ontime-event-times {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
margin-top: 3px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<%
|
||||
function formatMs(ms) {
|
||||
if (ms === null || ms === undefined) return '--:--';
|
||||
var neg = ms < 0;
|
||||
var abs = Math.abs(ms);
|
||||
var h = Math.floor(abs / 3600000);
|
||||
var m = Math.floor((abs % 3600000) / 60000);
|
||||
var s = Math.floor((abs % 60000) / 1000);
|
||||
var result = h > 0
|
||||
? h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0')
|
||||
: m + ':' + String(s).padStart(2, '0');
|
||||
return (neg ? '-' : '') + result;
|
||||
}
|
||||
function formatClock(ms) {
|
||||
if (ms === null || ms === undefined) return '';
|
||||
var h = Math.floor(ms / 3600000) % 24;
|
||||
var m = Math.floor((ms % 3600000) / 60000);
|
||||
return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
||||
}
|
||||
var playback = data.timer ? data.timer.playback : 'stop';
|
||||
var current = data.timer ? data.timer.current : null;
|
||||
var duration = data.timer ? data.timer.duration : null;
|
||||
var overtime = current !== null && current < 0;
|
||||
%>
|
||||
|
||||
<header>
|
||||
<h1><%= listName %></h1>
|
||||
</header>
|
||||
|
||||
<div class="ontime-top-bar <%= playback %><%= overtime ? ' overtime' : '' %>">
|
||||
<div class="ontime-top-section">
|
||||
<div class="ontime-top-label">Running timer</div>
|
||||
<div class="ontime-timer-value"><%= formatMs(current) %></div>
|
||||
<% if (data.timer && data.timer.addedTime && data.timer.addedTime !== 0) { %>
|
||||
<div class="ontime-added-time"><%= data.timer.addedTime > 0 ? '+' : '' %><%= Math.round(data.timer.addedTime / 1000) %>s</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (data.clock !== null && data.clock !== undefined) { %>
|
||||
<div class="ontime-top-section ontime-top-clock">
|
||||
<div class="ontime-top-label">Time now</div>
|
||||
<div class="ontime-clock-value"><%= formatClock(data.clock) %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (duration !== null && duration !== undefined && duration > 0) { %>
|
||||
<% var elapsed = data.timer && data.timer.elapsed !== null && data.timer.elapsed !== undefined ? data.timer.elapsed : null; %>
|
||||
<% var pct = elapsed !== null ? Math.max(0, Math.min(100, (elapsed / duration) * 100)) : 0; %>
|
||||
<div class="ontime-progress-bar"><div class="ontime-progress-fill" style="width:<%= Math.round(pct) %>%"></div></div>
|
||||
<% } %>
|
||||
|
||||
<% if (data.eventNow) { %>
|
||||
<div class="ontime-event ontime-event-now" style="<%= data.eventNow.colour ? 'border-left-color:' + data.eventNow.colour : '' %>">
|
||||
<div class="ontime-event-header">
|
||||
<span class="ontime-event-cue"><%= data.eventNow.cue || '' %></span>
|
||||
<span class="ontime-event-label">Now</span>
|
||||
</div>
|
||||
<div class="ontime-event-title"><%= data.eventNow.title || '' %></div>
|
||||
<% if (data.eventNow.timeStart !== undefined && data.eventNow.timeEnd !== undefined) { %>
|
||||
<div class="ontime-event-times"><%= formatClock(data.eventNow.timeStart) %> – <%= formatClock(data.eventNow.timeEnd) %></div>
|
||||
<% } %>
|
||||
<% if (data.eventNow.note) { %>
|
||||
<div class="ontime-event-note"><%= data.eventNow.note %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (data.eventNext) { %>
|
||||
<div class="ontime-event ontime-event-next" style="<%= data.eventNext.colour ? 'border-left-color:' + data.eventNext.colour : '' %>">
|
||||
<div class="ontime-event-header">
|
||||
<span class="ontime-event-cue"><%= data.eventNext.cue || '' %></span>
|
||||
<span class="ontime-event-label">Next</span>
|
||||
</div>
|
||||
<div class="ontime-event-title"><%= data.eventNext.title || '' %></div>
|
||||
<% if (data.eventNext.timeStart !== undefined && data.eventNext.timeEnd !== undefined) { %>
|
||||
<div class="ontime-event-times"><%= formatClock(data.eventNext.timeStart) %> – <%= formatClock(data.eventNext.timeEnd) %></div>
|
||||
<% } %>
|
||||
<% if (data.eventNext.note) { %>
|
||||
<div class="ontime-event-note"><%= data.eventNext.note %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
Reference in New Issue
Block a user