diff --git a/README.md b/README.md index db8324f..5f2f02a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # ontime-timeroverlay -Simple overlay for the Ontime Timer to add specific things \ No newline at end of file +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 \ No newline at end of file diff --git a/timeroverlay/app.js b/timeroverlay/app.js new file mode 100644 index 0000000..518c5a0 --- /dev/null +++ b/timeroverlay/app.js @@ -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}` : ''; +} \ No newline at end of file diff --git a/timeroverlay/index.html b/timeroverlay/index.html new file mode 100644 index 0000000..d4e6c75 --- /dev/null +++ b/timeroverlay/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + +
+ + +
+ + \ No newline at end of file diff --git a/timeroverlay/style.css b/timeroverlay/style.css new file mode 100644 index 0000000..e29910e --- /dev/null +++ b/timeroverlay/style.css @@ -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; +} \ No newline at end of file