diff --git a/timeroverlaytouch/app.js b/timeroverlaytouch/app.js new file mode 100644 index 0000000..4f28a27 --- /dev/null +++ b/timeroverlaytouch/app.js @@ -0,0 +1,187 @@ +/*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`; +let websocket = null + +connectSocket(); + +let reconnectTimeout; +const reconnectInterval = 1000; +let reconnectAttempts = 0; + +/** + * Connects to the websocket server + * @param {string} socketUrl + */ +function connectSocket(socketUrl = userProvidedSocketUrl) { + 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"); + + localVars.nowid = payload.eventNow.id; + localVars.layoutimg = payload.eventNow.custom["layoutimage"] ?? null; + + let newready = payload.eventNow.custom["ready"] == "true"; + if (localVars.ready != newready) { + localVars.ready = newready; + updateReady(); + } + + newready = payload.eventNow.custom["stageready"] == "true"; + if (localVars.stageready != newready) { + localVars.stageready = newready; + updateStageButton(); + } + + imgelement.src = localVars.layoutimg; + if (localVars.layoutimg) { + imgelement.classList.remove("hidden"); + timerelement.classList.add("hidden"); + }else { + imgelement.classList.add("hidden"); + timerelement.classList.remove("hidden"); + } + } +} + +async function blink(id) { + document.getElementById(id).classList.add("blink"); + setTimeout(() => { + document.getElementById(id).classList.remove("blink"); + }, 3000); +} + +function onReadyClick() { + const message = { + tag: "change", + payload: { + [localVars.nowid]: { + "custom:stageready": !localVars.stageready + } + } + }; + websocket.send(JSON.stringify(message)); +} + +function updateStageButton() { + let stagereadybutton = document.getElementById("stageready"); + if (localVars.stageready) { + stagereadybutton.classList.remove("redoutline"); + stagereadybutton.classList.add("greenoutline"); + }else { + stagereadybutton.classList.add("redoutline"); + stagereadybutton.classList.remove("greenoutline"); + } +} + +function updateReady() { + let fohreadybutton = document.getElementById("fohready"); + let container = document.getElementById("container"); + + if (localVars.ready) { + container.classList.remove("redoutline"); + container.classList.add("greenoutline"); + blink("container") + fohreadybutton.classList.remove("redoutline"); + fohreadybutton.classList.add("greenoutline"); + }else { + container.classList.add("redoutline"); + container.classList.remove("greenoutline"); + fohreadybutton.classList.add("redoutline"); + fohreadybutton.classList.remove("greenoutline"); + } +} + +/** + * 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/timeroverlaytouch/index.html b/timeroverlaytouch/index.html new file mode 100644 index 0000000..f865ca4 --- /dev/null +++ b/timeroverlaytouch/index.html @@ -0,0 +1,32 @@ + + + + + + +
+ + +