signageos-2.3.x-dev/templates/signageos-applet.html.twig
templates/signageos-applet.html.twig
<!doctype html>
<html lang="en" style="height:100%;">
<head>
<title>{{ sitename }}</title>
</head>
<body style="height:100%;margin:0;padding:0;">
<div id="loadScreen">
<style>
#loadScreen {
height:100%;
padding:10%;
text-align:center;
background-color:{{ bgcolor }};
color:{{ fgcolor }};
}
#loadScreen svg {
width:100%;
max-height:60%;
height:auto;
}
</style>
<h1>{{ welcome }}</h1>
{{ logo|raw }}
<p id="index">... loading ...</p>
</div>
<div id="underlays"></div>
<img id="image" src="#" alt="image" style="display:none;"/>
<div id="video" style="display:none;"> </div>
<div id="html" style="display:none;"> </div>
<div id="overlays"></div>
<iframe id="browser"></iframe>
<div id="browserwidgets"><div class="close"></div></div>
<script type="application/ecmascript">
window.drupalSettings = {};
async function startApplet() {
String.prototype.hashCode = function(){
let hash = 0, i, char;
if (this.length === 0) return hash;
for (i = 0; i < this.length; i++) {
char = this.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
// Convert to 32bit integer.
hash = hash & hash;
}
return hash;
};
const myLog = function(info) {
if (sos.config.debug) {
console.log(info);
sos.command.dispatch({
type: 'DeviceLog.Debug',
payload: JSON.stringify(info)
});
}
};
const myError = function(error) {
myLog('ERROR ====================================================');
myLog(error);
sos.command.dispatch({
type: 'DeviceLog.Error',
payload: JSON.stringify(error)
});
};
const myReport = function(slide) {
sos.command.dispatch({
type: 'DeviceReport.Slide',
payload: slide
});
};
const updateDynamicContent = function(contentItems, item, url, timeout) {
setTimeout(async function () {
if (!paused) {
myLog('Downloading dynamic content ' + item + ' from ' + url);
let fileName = 'dynamic-content-' + item;
const content = await readLocalFile(fileName, url, false);
myLog('Received ' + content.length + ' bytes');
await prepareContent(contentItems, item, content);
sos.offline.cache.deleteFile(fileName)
.catch(function (error) {
myError(error);
});
}
updateDynamicContent(contentItems, item, url, sos.config.refreshInterval * 1000);
}, timeout);
};
const updateDynamicBlock = function(item, blockid, url, timeout) {
setTimeout(async function () {
if (!paused) {
myLog('Downloading dynamic block ' + blockid);
let fileName = 'dynamic-block-' + blockid;
const content = await readLocalFile(fileName, url, false);
myLog('Received ' + content.length + ' bytes');
let parser = new DOMParser();
let dom = parser.parseFromString(content, 'text/html');
let newContent = dom.querySelector('div[data-drupal-digitalsignage-dynamic="true"]');
item.innerHTML = newContent.innerHTML;
sos.offline.cache.deleteFile(fileName)
.catch(function (error) {
myError(error);
});
}
updateDynamicBlock(item, blockid, url, sos.config.refreshInterval * 1000);
}, timeout);
};
const loadSchedule = async function(forceReload) {
myLog('Loading schedule');
let fileName = 'schedule.json';
if ((forceReload || sos.config.reloadcontent) && existingFileUids.includes(fileName)) {
myLog('Deleting existing schedule');
await sos.offline.cache.deleteFile(fileName)
.catch(function (error) {
myError(error);
});
}
const content = await readLocalFile(fileName, sos.config.api + '?mode=schedule&deviceId=' + sos.config.deviceId, true);
myLog('Received:');
myLog(content);
contentAssets = content.assets;
schedule = content.schedule;
emergencyentities = content.emergencyentities;
underlays = content.underlays;
overlays = content.overlays;
};
const readLocalFile = async function(fileName, url, json) {
myLog('Loading ' + fileName + ' from ' + url);
await sos.offline.cache.loadOrSaveFile(fileName, url, sos.config.httpHeader)
.catch(function (error) {
myError(error);
});
let {filePath} = await sos.offline.cache.loadFile(fileName);
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', filePath);
xhr.onload = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 0) {
let data;
if (json) {
try {
data = JSON.parse(xhr.responseText);
}
catch (e) {
myError("Invalid json: " + xhr.responseText);
data = [];
}
}
else {
data = xhr.responseText;
}
resolve(data);
} else {
myError("Error loading file " + filePath);
myReport('error');
// reject("Error loading file " + filePath);
if (json) {
resolve([]);
}
else {
resolve('');
}
}
}
};
xhr.send();
});
};
const prepareAssetFile = async function(uri, uid, type) {
if (sos.config.reloadassets && existingFileUids.includes(uid)) {
await sos.offline.cache.deleteFile(uid)
.catch(function (error) {
myError(error);
});
}
return {
"uri": uri,
"uid": uid,
"type": type,
"headers": sos.config.httpHeader,
"flags": [sos.offline.flags.append(document.head)]
};
};
const loadAssets = async function() {
myLog('Loading assets');
let scripts = [];
scripts.push(await prepareAssetFile(sos.config.api + '?mode=load&type=css&deviceId=' + sos.config.deviceId, 'styles.css', sos.offline.types.css));
for (const script in sos.config.scripts) {
if (sos.config.scripts.hasOwnProperty(script)) {
scripts.push(await prepareAssetFile(sos.config.scripts[script]['uri'], sos.config.scripts[script]['uid'], sos.offline.types.javascript));
}
}
let parser = new DOMParser();
let dom = parser.parseFromString(contentAssets, 'text/html');
let cssFiles = await extractFileLinks(dom, 'link', 'href');
let jsFiles = await extractFileLinks(dom, 'script', 'src');
let i;
for (i = 0; i < cssFiles.length; i++) {
let file = cssFiles[i];
scripts.push(await prepareAssetFile(file, file.hashCode() + '.css', sos.offline.types.css));
}
for (i = 0; i < jsFiles.length; i++) {
let file = jsFiles[i];
scripts.push(await prepareAssetFile(file, file.hashCode() + '.js', sos.offline.types.javascript));
}
let contentSettings = {};
let settingsElement = dom.querySelector('script[type="application/json"][data-drupal-selector="drupal-settings-json"]');
if (settingsElement !== null) {
contentSettings = JSON.parse(settingsElement.textContent);
}
else {
settingsElement = dom.createElement('script');
settingsElement.setAttribute('type', 'application/json');
settingsElement.setAttribute('data-drupal-selector', 'drupal-settings-json');
}
window.drupalSettings = Object.assign(sos.config.drupalSettings, contentSettings);
settingsElement.textContent = JSON.stringify(window.drupalSettings);
document.body.appendChild(settingsElement);
myLog(scripts);
await sos.offline.addFilesSync(scripts)
.catch(function (error) {
myError(error);
});
for (const font in sos.config.fonts) {
if (sos.config.fonts.hasOwnProperty(font)) {
sos.config.fonts[font]['append'] = document.head;
myLog("Loading font");
myLog(sos.config.fonts[font]);
await sos.offline.addFont(sos.config.fonts[font])
.catch(function (error) {
myError(error);
});
myLog("Received font " + sos.config.fonts[font]['uid']);
}
}
};
const extractFileLinks = async function(dom, tag, attr) {
let files = [];
let elements = dom.getElementsByTagName(tag);
let i;
for (i = 0; i < elements.length; i++) {
let element = elements[i];
if (element.getAttribute('data-sos-loaded') !== 'ok') {
element.setAttribute('data-sos-loaded', 'ok');
if (element.hasAttribute(attr)) {
files.push(sos.config.baseUrl + element.getAttribute(attr));
}
}
}
return files;
}
const loadImages = async function(html, tag) {
let parser = new DOMParser();
let dom = parser.parseFromString(html, 'text/html');
let elements = dom.getElementsByTagName(tag);
let i;
for (i = 0; i < elements.length; i++) {
let element = elements[i];
if (element.getAttribute('data-sos-loaded') !== 'ok') {
element.setAttribute('data-sos-loaded', 'ok');
if (element.hasAttribute('src')) {
let path = element.getAttribute('src').replace(" 1x", "");
let uid = "content-" + path.hashCode();
if (sos.config.reloadcontent && existingFileUids.includes(uid)) {
myLog("Deleting " + uid);
await sos.offline.cache.deleteFile(uid)
.catch(function (error) {
myError(error);
});
}
myLog("Loading " + path);
let uri = sos.config.api + '?mode=load&type=content&deviceId=' + sos.config.deviceId + '&contentPath=' + btoa(path.replace("&", "&"));
myLog("Uri " + uri);
const {filePath} = await sos.offline.cache.loadOrSaveFile(uid, uri, sos.config.httpHeader)
.catch(function (error) {
myError(error);
});
element.setAttribute('src', filePath);
myLog("Received " + filePath);
}
}
}
return dom.body.innerHTML;
};
const prepareContent = async function(contentItems, item, content) {
content = await loadImages(content, 'img');
content = await loadImages(content, 'picture');
content = await loadImages(content, 'source');
if (contentItems[item].hasOwnProperty('content')) {
contentItems[item].content = content;
}
else {
contentItems[item] = content;
}
};
const loadContent = async function(contentItems) {
myLog('Loading content');
myLog(contentItems);
if (contentItems === undefined || contentItems[0] === undefined) {
return;
}
let ve = jQuery('#video');
for (const item in contentItems) {
if (contentItems.hasOwnProperty(item)) {
contentItems[item].uid = contentItems[item].entity.type + '_' + contentItems[item].entity.id;
if (contentItems[item].type !== 'html') {
if (sos.config.reloadcontent && existingFileUids.includes(contentItems[item].uid)) {
await sos.offline.cache.deleteFile(contentItems[item].uid)
.catch(function (error) {
myError(error);
});
}
contentItems[item].uri = sos.config.api + '?mode=load&type=' + contentItems[item].type + '&deviceId=' + sos.config.deviceId + '&entityType=' + contentItems[item].entity.type + '&entityId=' + contentItems[item].entity.id;
myLog("Loading item");
myLog("Uri " + contentItems[item].uri);
const {filePath} = await sos.offline.cache.loadOrSaveFile(contentItems[item].uid, contentItems[item].uri, sos.config.httpHeader)
.catch(function (error) {
myError(error);
});
if (filePath) {
myLog("Received " + filePath);
contentItems[item].filePath = filePath;
if (contentItems[item].type === 'video') {
contentItems[item].arguments = [contentItems[item].filePath, 0, 0, ve.width(), ve.height()];
}
}
else {
contentItems[item].filePath = false;
myError("Unable to receive " + contentItems[item].uri);
}
}
else {
await prepareContent(contentItems, item, contentItems[item].content);
}
if (contentItems[item].dynamic) {
// TODO: Cancel updates when refreshing schedule.
updateDynamicContent(contentItems, item, sos.config.api + '?mode=load&type=' + contentItems[item].type + '&deviceId=' + sos.config.deviceId + '&entityType=' + contentItems[item].entity.type + '&entityId=' + contentItems[item].entity.id, 0);
}
}
}
};
const playSchedule = async function(playRound) {
introElement.style.display = 'none';
contentElement.innerHTML = '';
let previousIndex;
let currentIndex = 0;
let singleSlide = (schedule.length === 1);
let video = false;
let videoArguments = [];
if (singleSlide && schedule[0].dynamic) {
schedule[0].duration = sos.config.refreshInterval * 1.5;
singleSlide = false;
}
myReport('Starting play');
do {
if (paused) {
myLog('Pausing');
let timer = Drupal.digital_signage_timer.setInitialTimeout(10);
await timer.promise;
continue;
}
let previous = typeof previousIndex === 'undefined' ? undefined : schedule[previousIndex];
let current = schedule[currentIndex];
if (singleSlide && video) {
myLog('Restarting video');
await sos.video.play(...videoArguments);
}
else {
myLog('Displaying slide ' + currentIndex);
myReport(currentIndex);
if (video) {
await sos.video.stop(...videoArguments);
video = false;
videoArguments = []
}
// play current
if (current.type === 'video') {
video = true;
videoArguments = current.arguments
videoElement.style.display = 'block';
await sos.video.play(...current.arguments);
}
else if (current.type === 'image' && current.filePath) {
imageElement.src = current.filePath;
imageElement.style.display = 'block';
}
else if (current.type === 'html') {
htmlElement.innerHTML = current.content;
htmlElement.style.display = 'block';
Drupal.attachBehaviors();
let videos = jQuery('video');
if (videos.length > 0) {
video = true;
let position = videos.first().offset();
myLog(jQuery('source', videos[0]).attr('src'));
videoArguments = [jQuery('source', videos[0]).attr('src'), position.left, position.top, videos[0].offsetWidth, videos[0].offsetHeight];
await sos.video.play(...videoArguments);
videos.first().hide();
}
}
else {
myError('Unrecognized item type: ' + current.type);
}
// stop previous
if (previous) {
if (previous.type === 'video') {
if (current.type !== 'video') {
videoElement.style.display = 'none';
}
await sos.video.stop(...previous.arguments);
}
else if (previous.type === 'image' && current.type !== 'image') {
imageElement.style.display = 'none';
}
else if (previous.type === 'html' && current.type !== 'html') {
htmlElement.style.display = 'none';
}
}
if (singleSlide && !video) {
myLog('Finishing play, only one slide');
return;
}
}
// prepare next
let timer = Drupal.digital_signage_timer.setInitialTimeout(current.duration);
if (singleSlide && video) {
timer.promise = sos.video.onceEnded(...videoArguments);
}
else {
const nextIndex = (currentIndex + 1) % schedule.length;
if (current.type === 'video') {
timer.promise = sos.video.onceEnded(...current.arguments);
const next = schedule[nextIndex];
if (next.type === 'video') {
await sos.video.prepare(...next.arguments);
}
}
else if (current.type === 'image') {
// Nothing to do, keep the predefined promise.
}
else if (current.type === 'html') {
if (video) {
timer.promise = sos.video.onceEnded(...videoArguments);
}
else {
// Nothing to do, keep the predefined promise.
}
}
else {
myError('Unrecognized item type: ' + current.type);
timer.promise = Promise.resolve();
}
previousIndex = currentIndex;
currentIndex = nextIndex;
}
await timer.promise;
} while (appletPlayRound === playRound);
if (video) {
await sos.video.stop(...videoArguments);
}
};
const readLoadedFiles = async function() {
myLog("Read loaded files");
existingFileUids = await sos.offline.cache.listFiles()
.catch(function (error) {
myError(error);
});
myLog(existingFileUids);
};
const checkEmergencyMode = async function() {
myLog('Checking emergency mode');
if (sos.config.emergencyEntity.hasOwnProperty('type') && sos.config.emergencyEntity.hasOwnProperty('id')) {
myLog('Emergency mode gets enabled');
myLog(sos.config.emergencyEntity);
for (const item in emergencyentities) {
if (emergencyentities.hasOwnProperty(item)) {
if (emergencyentities[item].entity.type === sos.config.emergencyEntity.type && emergencyentities[item].entity.id === sos.config.emergencyEntity.id) {
if (originalSchedule.length === 0) {
originalSchedule = schedule;
}
schedule = [];
schedule[0] = emergencyentities[item];
return;
}
}
}
myError('Emergency entity is not available.');
}
};
const runSchedule = async function(forceReload) {
appletPlayRound++;
videoElement.style.display = 'none';
imageElement.style.display = 'none';
htmlElement.style.display = 'none';
introElement.style.display = 'block';
contentElement.innerHTML = 'Loading content...';
underlaysElement.innerHTML = '';
overlaysElement.innerHTML = '';
await readLoadedFiles();
await loadSchedule(forceReload);
myLog(contentAssets);
myLog(schedule);
myLog(emergencyentities);
myLog(underlays);
myLog(overlays);
await checkEmergencyMode();
await loadAssets();
if (schedule === undefined || schedule[0] === undefined) {
// We haven't got any schedules yet, so let's quit.
contentElement.innerHTML = 'No content available at this time.';
myError('No content in the schedule yet.');
}
await loadContent(emergencyentities);
await loadContent(schedule);
await loadContent(originalSchedule);
myLog('Handle underlays');
let i = 0;
for (i = 0; i < underlays.length; i++) {
await prepareContent(underlays, i, underlays[i]);
underlaysElement.innerHTML = underlaysElement.innerHTML + underlays[i];
}
myLog('Handle overlays');
for (i = 0; i < overlays.length; i++) {
await prepareContent(overlays, i, overlays[i]);
overlaysElement.innerHTML = overlaysElement.innerHTML + overlays[i];
}
myLog('Handle dynamic content');
let dynamicBlocks = document.querySelectorAll('div[data-drupal-digitalsignage-dynamic="true"]');
for (i = 0; i < dynamicBlocks.length; i++) {
dynamicBlocks[i].innerHTML = '';
let blockid = dynamicBlocks[i].getAttribute('data-drupal-digitalsignage-blockid');
updateDynamicBlock(dynamicBlocks[i], blockid, sos.config.api + '/block/' + blockid, 0);
}
myLog('Handle Links');
let links = document.getElementsByTagName('a');
myLog(links);
for (i = 0; i < links.length; i++) {
myLog('Link ' + i);
links.item(i).addEventListener('click', async function (event) {
myLog('Received click on link ' + i);
event.preventDefault();
paused = true;
browserElement.src = this.getAttribute('href');
browserElement.setAttribute('class', 'show');
browserWidgets.setAttribute('class', 'show');
Drupal.digital_signage_timer.popupTimer = Drupal.digital_signage_timer.setInitialTimeout(30);
await Drupal.digital_signage_timer.popupTimer.promise;
myLog('Timeout');
browserWidgets.getElementsByClassName('close')[0].click();
});
}
myLog('Restore');
sos.restore();
myLog('Play schedule');
playSchedule(appletPlayRound)
.then(response => myLog('Play schedule finished'));
await readLoadedFiles();
};
const introElement = document.getElementById('loadScreen');
const contentElement = document.getElementById('index');
const imageElement = document.getElementById('image');
const videoElement = document.getElementById('video');
const htmlElement = document.getElementById('html');
const underlaysElement = document.getElementById('underlays');
const overlaysElement = document.getElementById('overlays');
const browserElement = document.getElementById('browser');
const browserWidgets = document.getElementById('browserwidgets');
browserWidgets.getElementsByClassName('close')[0].addEventListener('click', function (event) {
myLog('Received click on close browser');
browserElement.removeAttribute('class');
browserWidgets.removeAttribute('class');
paused = false;
});
contentElement.innerHTML = 'sOS is loaded';
await sos.onReady();
contentElement.innerHTML = 'sOS is ready';
myLog('sOS is ready');
myLog(sos.config);
let paused = false;
let existingFileUids = [];
let contentAssets = '';
let schedule = {};
let originalSchedule = {};
let emergencyentities = {};
let appletPlayRound = 0;
let underlays = {};
let overlays = {};
runSchedule(false)
.then(response => myLog('Schedule loaded and play started'));
sos.command.onCommand((commandEvent) => {
myLog('Received command');
myLog(commandEvent.command.payload);
switch (commandEvent.command.type) {
case 'UpdateConfig':
myLog('Command: Update Config');
sos.config = commandEvent.command.payload.config;
runSchedule(commandEvent.command.payload.reload);
break;
case 'EmergencyModeSet':
myLog('Command: Set emergency mode');
sos.config.emergencyEntity = {
'type': commandEvent.command.payload.type,
'id': commandEvent.command.payload.id,
};
runSchedule(false);
break;
case 'EmergencyModeDisable':
myLog('Command: Disable emergency mode');
sos.config.emergencyEntity = {};
runSchedule(false);
break;
default:
myError('Unknown command');
myError(commandEvent);
}
});
}
typeof sos !== 'undefined' ?
startApplet() :
window.addEventListener('sos.loaded', startApplet);
</script>
</body>
</html>
