feat(overlay): add layoutimg display and border blink on readay

This commit is contained in:
2026-02-26 08:38:12 +01:00
parent a84773ca64
commit f481611b1e
4 changed files with 229 additions and 1 deletions

View File

@@ -1,3 +1,7 @@
# ontime-timeroverlay
Simple overlay for the Ontime Timer to add specific things
### Custom Fields
- **layoutimage**: display the image fullscreen on the timeroverlay view
- **ready**: turns border green or red

143
timeroverlay/app.js Normal file
View File

@@ -0,0 +1,143 @@
/*eslint-env browser*/
/**
* This is a very minimal example for a websocket client
* You could use this as a starting point to creating your own interfaces
*/
// Data that the user needs to provide depending on the Ontime URL
const isSecure = window.location.protocol === 'https:';
const userProvidedSocketUrl = `${isSecure ? 'wss' : 'ws'}://${window.location.host}${getStageHash()}/ws`;
connectSocket();
let reconnectTimeout;
const reconnectInterval = 1000;
let reconnectAttempts = 0;
/**
* Connects to the websocket server
* @param {string} socketUrl
*/
function connectSocket(socketUrl = userProvidedSocketUrl) {
const websocket = new WebSocket(socketUrl);
websocket.onopen = () => {
clearTimeout(reconnectTimeout);
reconnectAttempts = 0;
console.warn('WebSocket connected');
};
websocket.onclose = () => {
console.warn('WebSocket disconnected');
reconnectTimeout = setTimeout(() => {
console.warn(`WebSocket: attempting reconnect ${reconnectAttempts}`);
if (websocket && websocket.readyState === WebSocket.CLOSED) {
reconnectAttempts += 1;
connectSocket();
}
}, reconnectInterval);
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
websocket.onmessage = (event) => {
// all objects from ontime are structured with tag and payload
const { tag, payload } = JSON.parse(event.data);
/**
* runtime-data is sent
* - on connect with the full state
* - and then on every update with a patch
*/
if (tag === 'runtime-data') {
handleOntimePayload(payload);
}
};
}
let localData = {};
let localVars = {};
/**
* Handles the ontime payload updates
* @param {object} payload - The payload object containing the updates
*/
function handleOntimePayload(payload) {
// 1. apply the patch into your local copy of the data
localData = { ...localData, ...payload };
if ('eventNow' in payload) {
let imgelement = document.getElementById("image");
let timerelement = document.getElementById("timer");
let container = document.getElementById("container");
localVars.layoutimg = payload.eventNow.custom["layoutimage"] ?? null;
imgelement.src = localVars.layoutimg;
if (localVars.layoutimg) {
imgelement.classList.remove("hidden");
timerelement.classList.add("hidden");
}else {
imgelement.classList.add("hidden");
timerelement.classList.remove("hidden");
}
let ready = payload.eventNow.custom["ready"];
if (ready) {
container.classList.remove("redoutline");
container.classList.add("greenoutline");
blink("container")
}else {
container.classList.add("redoutline");
container.classList.remove("greenoutline");
}
}
}
async function blink(id) {
document.getElementById(id).classList.add("blink");
setTimeout(() => {
document.getElementById(id).classList.remove("blink");
}, 3000);
}
/**
* Updates the DOM with a given payload
* @param {string} field - The runtime data field
* @param {object} payload - The patch object for the field
*/
function updateDOM(field, payload) {
const domElement = document.getElementById(field);
if (domElement) {
domElement.innerText = payload;
}
}
/**
* Stringifies an object into a pretty string
* @param {object} data - The data object to format
* @returns {string} The formatted data string
*/
function formatObject(data) {
return JSON.stringify(data, null, 2);
}
/**
* Utility to handle a demo deployed in an ontime stage
* You can likely ignore this in your app
*
* an url looks like
* https://cloud.getontime.no/stage-hash/external/demo/ -> /stage-hash
* @returns {string} - The stage hash if the app is running in an ontime stage
*/
function getStageHash() {
const href = window.location.href;
if (!href.includes('getontime.no')) {
return '';
}
const hash = href.split('/');
const stageHash = hash.at(3);
return stageHash ? `/${stageHash}` : '';
}

27
timeroverlay/index.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]> <html class="no-js"> <!--<![endif]-->
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<script src="app.js" async defer></script>
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div id="container" class="container redoutline">
<img id="image" class="image hidden" src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdn.pixabay.com%2Fphoto%2F2018%2F01%2F05%2F00%2F20%2Ftest-image-3061864_1280.png&f=1&nofb=1&ipt=5f164ea7108fd967454bd94e6ad0022588b6ac82819b88cc790551694bb97522" alt="no image">
<iframe id="timer" class="timer" src="/timer" frameborder="0"></iframe>
</div>
</body>
</html>

54
timeroverlay/style.css Normal file
View File

@@ -0,0 +1,54 @@
body {
margin: 0;
font-family: var(--font-family-override, "Open Sans", "Segoe UI", sans-serif);
font-size: 2vh;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}
.container {
width: 100%;
height: 100%;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(255, 255, 255, 0.45);
}
.redoutline {
outline: 1vw solid #e7625c;
outline-offset: -1vw;
}
.greenoutline {
outline: 1vw solid #4fbe4c;
outline-offset: -1vw;
}
.image,
.timer {
object-fit: contain;
width: 100%;
height: 100%;
}
.blink {
animation: blink 1s ease-in-out 3;
}
@keyframes blink {
0% {
outline-color: transparent;
}
50% {
outline-color: #4fbe4c
}
to {
outline-color: transparent
}
}
.hidden {
display: none;
}