2832 lines
107 KiB
JavaScript
2832 lines
107 KiB
JavaScript
// ***************************************
|
|
// Client javascript for SPX
|
|
// ***************************************
|
|
// (c) 2020-2024 SPX Graphics
|
|
// ***************************************
|
|
var socket = io();
|
|
|
|
// Global App State
|
|
let APPSTATE = "INIT"; // see AppState()
|
|
let sortable = ""; // a global sortable object so can be disabled at will, see spxinit()
|
|
let messageSliderOutTimer // a global var for message slider timeout
|
|
document.onkeydown = checkKey;
|
|
|
|
socket.on('connect', function () {
|
|
// Added in 1.1.0:
|
|
if (document.getElementById('logo') && document.getElementById('logo_off')) {
|
|
document.getElementById('logo').src=document.getElementById('logo_on').src;
|
|
document.body.style="pointer-events: auto;" // restore clickability
|
|
hideMessageSlider();
|
|
}
|
|
}); // end connect
|
|
|
|
socket.on('SPXMessage2Extension', function (data) {
|
|
// Handles messages coming from server to an extension.
|
|
// All SPX extensions must have 3 tags in their HTML:
|
|
//
|
|
// <script src="/js/socket.io.js"></script>
|
|
// <script>var socket = io();</script>
|
|
// <script src="/js/spx_gc.js"></script>
|
|
//
|
|
// Also if the extension JS code is a module the
|
|
// functions will need to be exposed to the window scope.
|
|
// This is done by adding the following line to the end
|
|
// of the module:
|
|
//
|
|
// window.functionName = functionName;
|
|
//
|
|
if (typeof window[data.function] === "function") {
|
|
// console.log('API invokeExtensionFunction called --> ' + data.function + '(' + data.params + ')');
|
|
window[data.function](data.params);
|
|
} else {
|
|
console.error('Function not found in extension [' + data.function + ']. Is it exposed?');
|
|
}
|
|
|
|
}); // end SPXMessage2Extension
|
|
|
|
socket.on('SPXMessage2Client', function (data) {
|
|
// Handles messages coming from server to this client.
|
|
// All comms using 'SPXMessage2Client' as a conduit with data object and
|
|
// data.spxcmd as function identifier. Additional object values are payload.
|
|
|
|
// console.log('SPXMessage2Client received', data)
|
|
|
|
switch (data.spxcmd) {
|
|
case 'notifyMultipleControllers':
|
|
let notification = document.getElementById('severalDetected');
|
|
if (notification) { // controller view
|
|
if (data.count && data.count>1) {
|
|
console.log(data.count + ' SPX Controllers detected by the server. Displaying notification.');
|
|
notification.style.display = 'flex';
|
|
} else {
|
|
notification.style.display = 'none';
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'updateServerIndicator':
|
|
e = document.getElementById(data.indicator);
|
|
if (e) {e.style.color = data.color;}
|
|
break;
|
|
|
|
case 'updateStatusText':
|
|
if (document.getElementById('statusbar')) {
|
|
document.getElementById('statusbar').innerText = data.status;
|
|
clearTimeout(localStorage.getItem('SPX_CT_StatusTimer'));
|
|
statusTimer = setTimeout(function () { document.getElementById('statusbar').innerText = ''; }, 3000);
|
|
localStorage.setItem('SPX_CT_StatusTimer', statusTimer);
|
|
} else {
|
|
console.log('statusbar (element) not found');
|
|
}
|
|
break;
|
|
|
|
case 'showMessageSlider':
|
|
// Shows a message in the message slider, an extension may initiate this.
|
|
showMessageSlider(data.msg, data.type, data.persist);
|
|
// clearTimeout(localStorage.getItem('SPX_CT_StatusTimer'));
|
|
// statusTimer = setTimeout(function () { document.getElementById('statusbar').innerText = ''; }, 3000);
|
|
// localStorage.setItem('SPX_CT_StatusTimer', statusTimer);
|
|
break;
|
|
|
|
case 'clientLostNotification':
|
|
switch (data.clientName) {
|
|
case 'SPX_PROGRAM':
|
|
if (document.getElementById('toggleRendererWindowProgram')) {
|
|
console.log('SPX_PROGRAM client lost, toggling off');
|
|
document.getElementById('toggleRendererWindowProgram').checked = false;
|
|
} else {
|
|
console.log('SPX_PROGRAM client lost, no toggle found');
|
|
}
|
|
toggleNormalRenderer('normal');
|
|
break;
|
|
|
|
case 'SPX_PREVIEW':
|
|
if ( document.getElementById('toggleRendererWindowPreview') ) {
|
|
console.log('SPX_PREVIEW client lost, toggling off');
|
|
document.getElementById('toggleRendererWindowPreview').checked = false;
|
|
} else {
|
|
console.log('SPX_PREVIEW client lost, no toggle found');
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'updateRundownItem':
|
|
// This is a message to update a single item in the rundown.
|
|
// Used by API to update the UI after a playback command.
|
|
let item = getElementByEpoch(data.itemID);
|
|
if (item) {
|
|
let itemTemplate = item.querySelector('[name="RundownItem[relpath]"]').value;
|
|
if (data.relpath == itemTemplate) {
|
|
// console.log('Setting UI state of "' + data.itemID + '" forcefully to "' + data.command + '".');
|
|
playItem(item, data.command, true) // true = prevent playout, because API did it already
|
|
} else {
|
|
console.warn('Item template mismatch, not updating any indicators in the UI. Request', data);
|
|
}
|
|
} else {
|
|
console.warn('Requested item ' + data.itemID + ' not found. Not updating any indicators in the UI.');
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// console.log('Unknown SPXMessage2Client command: ' + data.command);
|
|
}
|
|
}); // end SPXMessage2Client
|
|
|
|
socket.on('SPXMessage2Controller', function (data) {
|
|
|
|
// Handles (API) messages coming from server to the SPX-GC Controller.
|
|
// All comms using 'SPXMessage2Controller' as a conduit with data object and
|
|
// data.APIcmd as function identifier. Additional object values are payload.
|
|
// Feature added in v.1.0.8.
|
|
|
|
let el = null;
|
|
let DomItemID;
|
|
switch (data.APIcmd) {
|
|
|
|
// Rundown commands below
|
|
|
|
case 'RundownLoad':
|
|
window.location.href = '/gc/' + data.file
|
|
break;
|
|
|
|
case 'RundownFocusFirst':
|
|
focusRow(0)
|
|
break;
|
|
|
|
case 'RundownFocusNext':
|
|
moveFocus(1)
|
|
break;
|
|
|
|
case 'RundownFocusPrevious':
|
|
moveFocus(-1)
|
|
break;
|
|
|
|
case 'RundownFocusLast':
|
|
let lastIndex = document.querySelectorAll('.itemrow').length-1;
|
|
focusRow(lastIndex)
|
|
break;
|
|
|
|
case 'RundownFocusByID':
|
|
focusRow(getElementByEpoch(data.itemID))
|
|
break;
|
|
|
|
case 'RundownStopAll':
|
|
stopAll()
|
|
break;
|
|
|
|
case 'RundownAllStatesToStopped':
|
|
// console.log('Turn lights off and set DOM variables. Storage handled separately.')
|
|
let items = document.querySelectorAll('.itemrow');
|
|
items.forEach((item,index) => {
|
|
item.setAttribute('data-spx-onair', 'false');
|
|
item.querySelector('[data-spx-name="icon"]').classList.remove('playTrue');
|
|
item.querySelector('[data-spx-name="icon"]').classList.remove('playAuto');
|
|
item.querySelector('[data-spx-name="icon"]').classList.add('playFalse');
|
|
});
|
|
break;
|
|
|
|
|
|
// Item commands below
|
|
|
|
case 'ItemPlay':
|
|
playItem(getFocusedRow(),'play')
|
|
break;
|
|
|
|
case 'ItemPlayID':
|
|
el = getElementByEpoch(data.itemID);
|
|
if (el) {
|
|
// console.log('ItemPlayID: Element found', data.itemID, el);
|
|
playItem(el,'play')
|
|
} else {
|
|
console.warn('ItemPlayID: Element not found', data.itemID);
|
|
}
|
|
break;
|
|
|
|
case 'ItemContinue':
|
|
nextItem(getFocusedRow())
|
|
break;
|
|
|
|
case 'ItemContinueID':
|
|
el = getElementByEpoch(data.itemID);
|
|
if (el) {
|
|
nextItem(el)
|
|
} else {
|
|
console.warn('ItemContinueID: Element not found', data.itemID);
|
|
}
|
|
break;
|
|
|
|
case 'ItemStop':
|
|
playItem(getFocusedRow(),'stop')
|
|
break;
|
|
|
|
case 'ItemStopID':
|
|
el = getElementByEpoch(data.itemID);
|
|
if (el) {
|
|
playItem(el,'stop')
|
|
} else {
|
|
console.warn('ItemStopID: Element not found', data.itemID);
|
|
}
|
|
break;
|
|
|
|
// Utils
|
|
case 'showMessageSlider':
|
|
showMessageSlider(data.msg, data.type, data.persist)
|
|
break;
|
|
|
|
default:
|
|
console.log('Unknown SPXMessage2Controller command: ' + data.APIcmd, data);
|
|
}
|
|
}); // end SPXMessage2Controller
|
|
|
|
socket.on('disconnect', function () {
|
|
// Added in 1.1.0
|
|
if (document.getElementById('logo') && document.getElementById('logo_off')) {
|
|
document.getElementById('logo').src=document.getElementById('logo_off').src;
|
|
showMessageSlider('Disconnected from SPX server', type='error', true)
|
|
document.body.style="pointer-events: none;" // disable clickability
|
|
}
|
|
}); // end disconnect
|
|
|
|
// see also spxInit()
|
|
// ##########################################################################################################
|
|
|
|
|
|
function rundownPopup(mode) {
|
|
// get current url
|
|
let url = window.location.href;
|
|
url = url + '/light'
|
|
window.open(url);
|
|
}
|
|
|
|
|
|
|
|
|
|
function addSelectedTemplate(idx) {
|
|
// alert('Selected index ' + idx);
|
|
data={};
|
|
data.command = "addItemToRundown";
|
|
data.foldername = document.getElementById('foldername').value;
|
|
data.listname = document.getElementById('filebasename').value;
|
|
data.datafile = document.getElementById('datafile').value;
|
|
data.templateindex = idx;
|
|
post('', data, 'post');
|
|
} // addSelectedTemplate
|
|
|
|
function addAllTemplatesToRundown() {
|
|
// Added in 1.2.0
|
|
data={};
|
|
data.command = "addAllItemsToRundown";
|
|
data.foldername = document.getElementById('foldername').value;
|
|
data.listname = document.getElementById('filebasename').value;
|
|
data.datafile = document.getElementById('datafile').value;
|
|
post('', data, 'post');
|
|
} // addAllTemplatesToRundown
|
|
|
|
function ajaxpost(urlPath, data, prepopulated='false'){
|
|
// This is a utility function using Axios to POST
|
|
// stuff to server. Here we handle response and
|
|
// status bar etc...
|
|
//
|
|
// Prepopulated -flag prevents reading content
|
|
// from json files and submits values as-is;
|
|
// this is used by show- and global extras.
|
|
|
|
if (prepopulated=='true'){
|
|
// console.log('Prepop YES');
|
|
data.prepopulated="true"
|
|
}else{
|
|
// console.log('Prepop NO');
|
|
data.prepopulated="false"
|
|
};
|
|
|
|
axios.post(urlPath, data)
|
|
.then(function (response) {
|
|
statusbar(response.data);
|
|
working('');
|
|
})
|
|
.catch(function (error) {
|
|
working('');
|
|
if (error.response) {
|
|
// statusbar('SPX server returned error, see console and logs','error')
|
|
statusbar(error.response.data,'error')
|
|
showMessageSlider(error.response.data, 'error')
|
|
// msg, type='info', persist=false
|
|
console.log(error.response.data);
|
|
console.log(error.response.status);
|
|
console.log(error.response.headers);
|
|
} else if (error.request) {
|
|
statusbar('SPX server connection error','error')
|
|
console.log(error.request);
|
|
} else {
|
|
// Something happened in setting up the request that triggered an Error
|
|
statusbar('SPX request error, see console','warn')
|
|
console.log('Error', error.message);
|
|
}
|
|
console.log(error.config);
|
|
});
|
|
} // ajaxpost ended
|
|
|
|
function ajaxpostform(urlPath, FormNro){
|
|
} // ajaxpostform ended
|
|
|
|
function aFunctionTest() {
|
|
// execute a test function on the server
|
|
// this gets triggered by menu > ping
|
|
console.log('Sending testfunction request to server. See server console / log with PING -identifier...')
|
|
AJAXGET('/CCG/testfunction/');
|
|
} //aFunctionTest
|
|
|
|
// Filename validation for project/ruindown prompts
|
|
// Added in 1.3.0
|
|
var isValid=(function(){
|
|
var rg1=/^[^\\/:\*\?"<>\|]+$/; // forbidden characters \ / : * ? " < > |
|
|
var rg2=/^\./; // cannot start with dot (.)
|
|
var rg3=/^\_/; // cannot start with underscore (_)
|
|
var rg4=/^(nul|prn|con|lpt[0-9]|com[0-9])(\.|$)/i; // forbidden file names
|
|
return function isValid(str){
|
|
return rg1.test(str)&&!rg2.test(str)&&!rg3.test(str)&&!rg3.test(str);
|
|
}
|
|
})();
|
|
|
|
|
|
function add(ModalID) {
|
|
// require ..... nothing
|
|
// returns ..... posts a name of a file to server which redirects
|
|
// Improved in 1.3.0 with validation
|
|
|
|
|
|
ModalOn(ModalID)
|
|
document.getElementById('rundownName').focus();
|
|
document.getElementById('rundownForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const rundownName = document.getElementById('rundownName').value.trim();
|
|
if (!isValid(rundownName)) {
|
|
alert("Please enter a valid rundown name, no special characters.");
|
|
return;
|
|
}
|
|
if (rundownName != null && rundownName != "") {
|
|
post('', { filebasename: rundownName }, 'post');
|
|
}
|
|
});
|
|
|
|
document.getElementById('cancelForm').addEventListener('click', function() {
|
|
ModalOff(ModalID);
|
|
})
|
|
|
|
} //add
|
|
|
|
|
|
function addshow(ModalID) {
|
|
// require ..... nothing
|
|
// returns ..... posts a name of a folder to server which redirects
|
|
// Improved in 1.3.0 with validation
|
|
|
|
ModalOn(ModalID);
|
|
|
|
document.getElementById('projectName').focus();
|
|
document.getElementById('projectForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const projectName = document.getElementById('projectName').value.trim();
|
|
const projectFormat = document.querySelector('input[name="projectFormat"]:checked').value;
|
|
// const projectFormat = document.getElementById('projectFormat').value; IF YOU WANT TO USE DROPDOWN INSTEAD OF RADIO BUTTONS
|
|
if (!isValid(projectName)) {
|
|
alert("Please enter a valid folder name, no special characters.");
|
|
return;
|
|
}
|
|
if (projectName != null && projectName != "") {
|
|
post('', { foldername: projectName, format: projectFormat }, 'post');
|
|
}
|
|
});
|
|
|
|
document.getElementById('cancelForm').addEventListener('click', function() {
|
|
ModalOff(ModalID);
|
|
});
|
|
|
|
} // addshow
|
|
|
|
async function AJAXGET(URL) {
|
|
// Sends a GET request to server.
|
|
// Async keyword added in 1.0.15.
|
|
// require ..... URL (such as '/CCG/testfunction/' )
|
|
// returns ..... true/false based on HTTP status codes, such as
|
|
// 200 OK
|
|
// 409 Conflict
|
|
var xmlhttp = new XMLHttpRequest();
|
|
xmlhttp.onreadystatechange = function () {
|
|
if (this.readyState == 4 && this.status == 200) {
|
|
if (this.responseText) {
|
|
// console.log('AJAX completed succesfully: ' + this.responseText)
|
|
return true
|
|
}
|
|
}
|
|
if (this.readyState == 4 && this.status != 200) {
|
|
console.log('Ajax error, status: ' + this.status + ', state: ' + this.readyState);
|
|
return false
|
|
}
|
|
};
|
|
xmlhttp.open("GET", URL);
|
|
xmlhttp.send();
|
|
} // AJAXGET ended
|
|
|
|
function applyTextEditChanges(event, e) {
|
|
// end editing a TG field
|
|
setMasterButtonStates(e.closest('.itemrow'));
|
|
if (APPSTATE != "EDITING") {
|
|
return;
|
|
}
|
|
if (event.keyCode == 13 || event.which == 13) { // 13 = enter key
|
|
ToggleExpand();
|
|
}
|
|
} // applyTextEditChanges
|
|
|
|
function AppState(NewState) {
|
|
// **********************************************************************
|
|
//
|
|
// Options for global application state. This can be utilized
|
|
// by several function around the application. You can use APPSTATE
|
|
// statement in devtools to see the currently active state...
|
|
//
|
|
// Usage: AppState('DEFAULT');
|
|
//
|
|
// AppState('DEFAULT'); // ......... nothing special
|
|
// AppState('INIT'); // ......... initialization of the page
|
|
// AppState('EDITING'); // ......... editing a text field (allow spaces)
|
|
//
|
|
// console.log('Old state: ' + APPSTATE, 'New state: ' + NewState);
|
|
|
|
// disable sorting while editing:
|
|
if (NewState == 'EDITING') {
|
|
if ( ife('identifier').value=="controller" ) {
|
|
sortable.option("disabled", true);
|
|
}
|
|
} else {
|
|
if ( ife('identifier').value=="controller" ) {
|
|
sortable.option("disabled", false);
|
|
}
|
|
}
|
|
APPSTATE = NewState;
|
|
//
|
|
// **********************************************************************
|
|
} // AppState
|
|
|
|
function CancelOutTimerIfRunning(CURITEM){
|
|
// if item has a running timeout attribute, then stop and clear it
|
|
let TimerID = CURITEM.getAttribute('data-spx-timerid') || '';
|
|
if (TimerID){
|
|
CURITEM.setAttribute('data-spx-timerid','');
|
|
clearTimeout(TimerID);
|
|
CURITEM.querySelector('[data-spx-name="icon"]').classList.remove('playAuto');
|
|
|
|
// Improved in v1.3.1
|
|
let smallIcon = document.getElementById('deleteSmall' + CURITEM.getAttribute('data-spx-index'));
|
|
let largeIcon = document.getElementById('deleteLarge' + CURITEM.getAttribute('data-spx-index'));
|
|
if (smallIcon) { smallIcon.style.display="inline-block" }
|
|
if (largeIcon) { largeIcon.style.display="block" }
|
|
}
|
|
} // CancelOutTimerIfRunning
|
|
|
|
function cas() {
|
|
// Opens the selected (or current) file in controller.
|
|
// Function works both in Edit and Lists -views.
|
|
// require ..... nothing
|
|
// returns ..... reloads the browser to another URL
|
|
let filename = "";
|
|
let element = "";
|
|
let folder = document.getElementById("hidden_folder").value;
|
|
element = document.getElementById("lists");
|
|
if (typeof (element) != 'undefined' && element != null) {
|
|
filename = element.value;
|
|
}
|
|
|
|
element = document.getElementById("filebasename");
|
|
if (typeof (element) != 'undefined' && element != null) {
|
|
filename = element.value;
|
|
}
|
|
if (!filename) {return}
|
|
document.location = '/gc/' + folder + "/" + filename;
|
|
} // cas ended
|
|
|
|
function cfg() {
|
|
// Show settings
|
|
// Called from Settings button in episodes page.
|
|
let foldname = document.getElementById("hidden_folder").value;
|
|
document.location = '/show/' + foldname + "/config";
|
|
} // del ended
|
|
|
|
function checkAllConnections() {
|
|
// require ..... nothing
|
|
// returns ..... forces server to check for CCG connections and pushes updates to indicators via socket.io
|
|
function aFunctionTest() {
|
|
// execute a test function on the server
|
|
AJAXGET('/CCG/testfunction/');
|
|
}
|
|
} //checkAllConnections
|
|
|
|
function checkKey(e) {
|
|
// require ..... keyboard event
|
|
// returns ..... executes keyboard shortcuts, such as applies "Return" to a text field while editing.
|
|
// keybord shortcuts (see https://keycode.info/)
|
|
|
|
// TODO: Add your desired keyboard shortcut functions here.
|
|
|
|
e = e || window.event;
|
|
|
|
// console.log('--- ' + e.keyCode + ' --- ' + APPSTATE);
|
|
|
|
// FIRST GENERIC keycodes for all situations
|
|
switch (e.keyCode) {
|
|
case 116: // F5
|
|
updateItem();
|
|
e.preventDefault(); // do not refresh browser
|
|
return;
|
|
break;
|
|
|
|
case 112: // F1
|
|
if (document.getElementById('overlayHelp')){
|
|
ModalOn('overlayHelp'); // only valid in Controller view.
|
|
e.preventDefault(); // do not open built-in help
|
|
}
|
|
return;
|
|
break;
|
|
|
|
case 27: // esc
|
|
if (document.getElementById('filebasename')) {
|
|
// we are in controller
|
|
stopAll(); // STOP all layers used by current rundown
|
|
}
|
|
|
|
case 68: // d and D
|
|
if (e.ctrlKey && document.getElementById('filebasename')) {
|
|
// Ctrl+D and we are in controller
|
|
duplicateRundownItem();
|
|
e.preventDefault(); // do use add to bookmarks
|
|
}
|
|
return;
|
|
break;
|
|
}
|
|
|
|
|
|
// THEN APPSTATE dependent keycodes
|
|
switch (APPSTATE) {
|
|
// EDITING (while editor open)
|
|
case "EDITING":
|
|
// console.log('Captured keypress while editing');
|
|
switch (e.keyCode) {
|
|
case 13: // enter
|
|
if ( document.activeElement.nodeName === "TEXTAREA" && document.hasFocus()) {
|
|
// console.log('Come on, we are multilining here');
|
|
return;
|
|
}
|
|
saveTemplateItemChangesByElement(getFocusedRow());
|
|
e.preventDefault();
|
|
break;
|
|
|
|
default: // any other key
|
|
if (document.querySelector('.inFocus')) {
|
|
document.querySelector('.inFocus').setAttribute("data-spx-changed","true");
|
|
}
|
|
break;
|
|
}
|
|
return;
|
|
break;
|
|
|
|
case "INIT":
|
|
return;
|
|
break;
|
|
|
|
|
|
// OTHER MODES, (while editor closed)
|
|
default:
|
|
// console.log('Captured keypress while NOT editing');
|
|
switch (e.keyCode) {
|
|
case 13: // enter
|
|
ToggleExpand()
|
|
e.preventDefault();
|
|
break;
|
|
|
|
case 32: // space
|
|
if (e.shiftKey) {
|
|
// Shift + Space = PLAY/STOP
|
|
playItem();
|
|
} else {
|
|
let item = getFocusedRow();
|
|
// Space = PLAY/CONTINUE/STOP
|
|
// Bug fix added in 1.1.1. Dblcheck if we are opened...
|
|
if (
|
|
item.querySelector('#Expanded')?.style
|
|
.display != 'none'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Added in v1.4.0 space works as PLAY/CONTINUE/STOP.
|
|
var stepsleft = parseInt(item?.getAttribute('data-spx-stepsleft'));
|
|
|
|
// Check if stepsleft isNan (not defined in templateDefinition) = PLAY/STOP, if == 1 (last step) = PLAY/STOP, if > 1 and not playing = PLAY
|
|
if
|
|
(isNaN(stepsleft)
|
|
|| stepsleft == 1
|
|
|| (stepsleft > 1 && item.getAttribute('data-spx-onair') != 'true')
|
|
|| (stepsleft == -1 && item.getAttribute('data-spx-onair') != 'true')
|
|
) {
|
|
playItem();
|
|
}
|
|
// Check if the item has steps, if so, continue to next step
|
|
else if (stepsleft > 1 || stepsleft == -1) {
|
|
nextItem();
|
|
}
|
|
}
|
|
|
|
e.preventDefault();
|
|
break;
|
|
|
|
case 38: // up
|
|
moveFocus(-1);
|
|
break;
|
|
|
|
case 40: // down
|
|
moveFocus(1);
|
|
break;
|
|
|
|
case 116: // F5
|
|
updateItem();
|
|
e.preventDefault(); // do not refresh browser
|
|
break;
|
|
case 20: // CapsLock
|
|
if (e.getModifierState("CapsLock")) {
|
|
showItemIDs(true);
|
|
} else {
|
|
showItemIDs(false);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
// setTimeout(function(){
|
|
// setMasterButtonStates(document.querySelector('.inFocus').getAttribute('data-spx-index'),'from checkKey');
|
|
// }, 20);
|
|
|
|
return;
|
|
} //checkKey
|
|
|
|
function clearAttributes(attName, attValue) {
|
|
// This is called from TGbuttonAction when TG Play button is
|
|
// pressed. This clears all found attName/attValues which
|
|
// forces the play/stop panel to show up upon focus.
|
|
// require ..... attribute name ("data-name") and value ("jack") to search for clearing
|
|
// returns ..... removes green arrow from TG list DOM
|
|
let TGrows = document.querySelectorAll('.itemrow');
|
|
TGrows.forEach(function (item) {
|
|
if (item.getAttribute(attName) == attValue) {
|
|
item.setAttribute(attName, '');
|
|
item.querySelector("#dragIcon").innerHTML = "☰"; // drag icon
|
|
item.querySelector("#dragIcon").classList.remove("green");
|
|
}
|
|
})
|
|
} // clearAttributes ended
|
|
|
|
function copyItemID(itemID, api='') {
|
|
let currentHost = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: '');
|
|
let str = itemID;
|
|
let inf = 'itemID';
|
|
switch (api.toUpperCase()) {
|
|
case 'PLAY':
|
|
str = currentHost + '/api/v1/item/play/' + itemID;
|
|
inf = 'API play command'
|
|
break;
|
|
|
|
case 'STOP':
|
|
str = currentHost + '/api/v1/item/stop/' + itemID;
|
|
inf = 'API stop command'
|
|
break;
|
|
|
|
case 'CONTINUE':
|
|
str = currentHost + '/api/v1/item/continue/' + itemID;
|
|
inf = 'API continue command'
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
|
|
}
|
|
copyText(str);
|
|
showMessageSlider('Copied ' + inf + ' to clipboard', 'happy');
|
|
showItemIDs(false)
|
|
} // copyItemID
|
|
|
|
// function toggleLRendererHandler(slider, targ) {
|
|
// let Cfgdata = {};
|
|
// Cfgdata.key = 'disableLocalRenderer'
|
|
// if (slider.checked) {
|
|
// document.getElementById(targ).innerText = 'YES';
|
|
// document.getElementById('previewIF').src = '/templates/empty.html';
|
|
// document.getElementById('LocalRendererDisabledNotification').style.opacity = 1;
|
|
// Cfgdata.val = true;
|
|
|
|
// } else {
|
|
// document.getElementById('previewIF').src = '/renderer';
|
|
// document.getElementById(targ).innerText = 'NO';
|
|
// document.getElementById('LocalRendererDisabledNotification').style.opacity = 0;
|
|
// Cfgdata.val = false;
|
|
// }
|
|
// ajaxpost('/gc/saveConfigChanges',Cfgdata);
|
|
// }
|
|
|
|
|
|
function toggleSwitchHandler(slider, targ, type) {
|
|
// When program/preview toggle buttons are clicked.
|
|
// console.log('toggleSwitchHandler', slider, targ);
|
|
let data = {}
|
|
data.type = type;
|
|
let handle = slider.querySelector('.vc-handle');
|
|
if (slider.checked) {
|
|
data.command = 'open';
|
|
document.getElementById(targ).innerText = 'ON';
|
|
} else {
|
|
data.command = 'close';
|
|
document.getElementById(targ).innerText = 'OFF';
|
|
}
|
|
handleRendererPopups(data)
|
|
} // toggleSwitchHandler
|
|
|
|
function toggleRundownSettings() {
|
|
// Toggle the rundown settings panel
|
|
let optionsZone = document.getElementById('rdOptionsCollapsible');
|
|
if (optionsZone.style.maxHeight){
|
|
optionsZone.style.maxHeight = null;
|
|
setTimeout(function(){optionsZone.style.display = 'none';}, 300);
|
|
} else {
|
|
optionsZone.style.display = 'block';
|
|
optionsZone.style.maxHeight = optionsZone.scrollHeight + "px";
|
|
}
|
|
} // toggleRundownSettings
|
|
|
|
function toggleTip(targetField, source, attr) {
|
|
document.getElementById(targetField).innerText = source.getAttribute(attr);
|
|
} // toggleTip
|
|
|
|
|
|
function toggleActive(ServerName='') {
|
|
// Will enable / disable commands to the server
|
|
// CCG/disable {server:'OVERLAY, disable:false}
|
|
let e = document.getElementById('server'+ ServerName);
|
|
let targetState = false;
|
|
if (e && e.classList.contains('disabledServer')) {
|
|
// Enable
|
|
e.classList.remove('disabledServer')
|
|
targetState = false
|
|
} else {
|
|
// Disable
|
|
e.classList.add('disabledServer')
|
|
targetState = true
|
|
}
|
|
|
|
let data = {};
|
|
data.server = ServerName;
|
|
data.disabled = targetState;
|
|
working('Changing server connection state to [' + targetState + ']...');
|
|
ajaxpost('/CCG/disable',data);
|
|
}
|
|
|
|
function clearUsedChannels(ServerName='') {
|
|
// Will call a command over API which will clear passed
|
|
// server or all by default.
|
|
let data = {};
|
|
data.server = ServerName;
|
|
data.foldername = document.getElementById('foldername').value;
|
|
data.datafile = document.getElementById('datafile').value;
|
|
working('Clearing ALL graphic channels used by program [' + data.foldername + ']...');
|
|
ajaxpost('/gc/clearPlayouts',data);
|
|
} //clearUsedChannels
|
|
|
|
function closePromo() {
|
|
// does just that and stores info to the localStorage so
|
|
// the same promo will not be show...
|
|
let e = document.getElementById('promo');
|
|
localStorage.setItem('SPX_ClosedPromo', e.getAttribute('data-promo-name'));
|
|
e.style.display = 'none';
|
|
}
|
|
|
|
function CollectJSONFromDOM() {
|
|
// iterate DOM and collect data to an array of objects
|
|
// This is called by SaveNewSortOrder().
|
|
// console.log('Collecting JSON');
|
|
let JSONdata = [];
|
|
let Items = document.querySelectorAll('.itemrow');
|
|
Items.forEach(function (item) {
|
|
var dataname = item.getElementsByTagName("input")[0].value;
|
|
var datatitl = item.getElementsByTagName("input")[1].value;
|
|
var Item = {};
|
|
Item.f0 = dataname;
|
|
Item.f1 = datatitl;
|
|
JSONdata.push(Item)
|
|
})
|
|
// console.log(JSONdata);
|
|
return JSONdata;
|
|
} // CollectJSONFromDOM ended
|
|
|
|
function continueUpdateStop(command, itemrow='') {
|
|
// request ..... command, itemrow (optional)
|
|
// returns ..... ajax response
|
|
// This is being called (at least) from custom-functions...
|
|
if (!itemrow) { itemrow = getFocusedRow(); }
|
|
switch (command) {
|
|
case 'stop':
|
|
// console.log('Stopping item', itemrow);
|
|
playItem(itemrow, 'stop');
|
|
break;
|
|
|
|
case 'continue':
|
|
console.log('------continue not implemented-----');
|
|
break;
|
|
|
|
case 'update':
|
|
console.log('------update not implemented-----');
|
|
break;
|
|
|
|
default:
|
|
console.log('Unknown command ['+ command +']');
|
|
break;
|
|
}
|
|
} // ContinueUpdateStop() ended
|
|
|
|
async function revealItemID(button) {
|
|
let item = button.closest('.itemrow')
|
|
let ID = item.getAttribute('data-spx-epoch');
|
|
copyText(ID)
|
|
// alert("Item ID " + ID + " was copied to clipboard.");
|
|
var newID = prompt("Item ID " + ID + " was copied to clipboard.\nYou can change the ID here:", ID);
|
|
if (newID != null && newID != ID) {
|
|
// alert("New ID " + newID)
|
|
let file = document.getElementById('datafile').value;
|
|
let URL = `/api/v1/changeItemID?rundownfile=${file}&ID=${ID}&newID=${newID}`
|
|
let IDchanged
|
|
|
|
axios.get(URL)
|
|
.then(function (response) {
|
|
showMessageSlider('Item renamed to ' + newID + '. Reload recommended.')
|
|
item.setAttribute('data-spx-epoch', newID);
|
|
item.querySelector('.copyid').setAttribute('onmouseover', `tip('Copy ID ${newID}')`);
|
|
})
|
|
.catch(function (error) {
|
|
showMessageSlider('Unable to rename. Use unique ID and try again.', 'error')
|
|
});
|
|
|
|
|
|
// if (IDchanged) {
|
|
// // success
|
|
// item.setAttribute('data-spx-epoch', newID);
|
|
// } else {
|
|
// alert("Unable to change ID to " + newID + " (" + IDchanged + ").\nMake sure the ID is unique and try again.");
|
|
// }
|
|
} else {
|
|
// alert("Same ID " + newID)
|
|
}
|
|
} // revealItemID
|
|
|
|
|
|
async function revealItemTiming(button) {
|
|
let curValue = button.textContent;
|
|
let ID = button.closest('.itemrow').getAttribute('data-spx-epoch');
|
|
var newValue = prompt("You can change timing here. Use 'manual', 'none' or millisecond values (1000=1s):", curValue);
|
|
if (newValue != null && newValue != curValue) {
|
|
let file = document.getElementById('datafile').value;
|
|
let URL = `/api/v1/changeItemData?rundownfile=${file}&ID=${ID}&key=out&newValue=${newValue}`
|
|
axios.get(URL)
|
|
.then(function (response) {
|
|
showMessageSlider('Item out mode changed to ' + newValue)
|
|
button.textContent = newValue;
|
|
button.closest('.itemrow').querySelectorAll('input[id^=out]')[0].value = newValue;
|
|
})
|
|
.catch(function (error) {
|
|
showMessageSlider('Unable to change value, please try again.', 'error')
|
|
});
|
|
}
|
|
} // revealItemTiming
|
|
|
|
async function revealItemLayer(button) {
|
|
let curValue = button.textContent;
|
|
let ID = button.closest('.itemrow').getAttribute('data-spx-epoch');
|
|
var newValue = prompt("You can change web playout layer here. Use any value between 1 and 5:", curValue);
|
|
|
|
if (newValue != null && newValue != curValue && !isNaN(newValue)) {
|
|
let wasOverLimit = newValue > 5;
|
|
newValue = Math.min(newValue, 5); // enforce max limit of 5
|
|
let file = document.getElementById('datafile').value;
|
|
let URL = `/api/v1/changeItemData?rundownfile=${file}&ID=${ID}&key=webplayout&newValue=${newValue}`
|
|
axios.get(URL)
|
|
.then(function (response) {
|
|
if (wasOverLimit) {
|
|
showMessageSlider('Item webplay layer must be 5 or less, changed to 5 ', "warn")
|
|
} else {
|
|
showMessageSlider('Item webplay layer changed to ' + newValue)
|
|
}
|
|
button.textContent = newValue;
|
|
button.closest('.itemrow').querySelectorAll('input[id^=webplayout]')[0].value = newValue;
|
|
// document.querySelectorAll('.itemrow')[0].querySelectorAll('input[id^=out]')[0];
|
|
})
|
|
.catch(function (error) {
|
|
showMessageSlider('Unable to change value, please try again.', 'error')
|
|
});
|
|
} else {
|
|
showMessageSlider('Unable to change value, please try again.', 'error')
|
|
}
|
|
} // revealItemLayer
|
|
|
|
function copyRendererUrl(preview=false) {
|
|
|
|
let page = '/renderer/';
|
|
if (preview) {
|
|
page = '/renderer?preview=true';
|
|
}
|
|
var RendererUrl = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: '') + page;
|
|
copyText(RendererUrl);
|
|
showMessageSlider('Copied URL to clipboard', 'happy');
|
|
} // copyRendererUrl ended
|
|
|
|
function copyText(txt) {
|
|
const temp = document.createElement('input');
|
|
document.body.appendChild(temp);
|
|
temp.value = txt;
|
|
temp.select();
|
|
try {
|
|
var successful = document.execCommand('copy');
|
|
var msg = successful ? 'successful' : 'unsuccessful';
|
|
// console.log('Copy command was ' + msg);
|
|
} catch (err) {
|
|
// console.log('Oops, unable to copy');
|
|
}
|
|
temp.parentNode.removeChild(temp);
|
|
} // copyText ended
|
|
|
|
function heartbeat(dd, getOnly = false) { // 36 24 36 hey
|
|
// see notes TDT=00d86114 (/..es2/heartbeat)
|
|
let nw, st, key, val, dif;
|
|
let fD = '|';
|
|
let vD = ':';
|
|
let eK = 'TS'
|
|
let ls = 'SPXGC_UI_stats';
|
|
let or = localStorage.getItem(ls) || eK + vD + "0" + fD + dd + vD + "0";
|
|
|
|
if (getOnly) { return or; }
|
|
|
|
let ar = or.split(fD);
|
|
let ok = false;
|
|
let rp = false;
|
|
ar.every(function(f,idx) {
|
|
key = f.split(vD)[0]; val = f.split(vD)[1];
|
|
if (key==eK) {
|
|
dif = Math.round((Date.now()-val)/(1000*60*60*24));
|
|
if (Math.round((Date.now()-val)/(1000*60*60*24))>0) { rp = true; };
|
|
// ###################
|
|
// rp = true; // debug
|
|
// ###################
|
|
ar[idx]=eK + vD + Date.now();
|
|
}
|
|
if (key==dd) {
|
|
nw = parseInt(val)+1;
|
|
ar[idx]=key + vD + nw;
|
|
ok = true; return false
|
|
} else {
|
|
ok = false; return true
|
|
}
|
|
});
|
|
if ( !ok ) { ar.push(dd + vD + '1'); }
|
|
st = ar.join(fD);
|
|
if (rp) {
|
|
// FIXME:
|
|
// console.log('Submit ' + or)
|
|
getMessages2(or)
|
|
st = eK + vD + Date.now() + fD + dd + vD + 1;
|
|
} else {
|
|
// FIXME:
|
|
// console.log('Resume ' + st)
|
|
}
|
|
localStorage.setItem(ls, st)
|
|
} // dirty deeds done.
|
|
|
|
function del() {
|
|
// Episodes -view
|
|
// Delete a datafile from disk.
|
|
// Called from delete button in lists page.
|
|
var filename = document.getElementById('lists').value;
|
|
if (!filename) {return}
|
|
let foldname = document.getElementById("hidden_folder").value;
|
|
|
|
if (confirm('SURE?! Delete "' + filename + '" and all it\'s data?')) {
|
|
fetch('/show/' + foldname + "/" + filename, { method: 'DELETE' });
|
|
setTimeout(function(){ document.location = '/show/' + foldname; }, 500);
|
|
}
|
|
} // del ended
|
|
|
|
function delshow() {
|
|
// Shows -view
|
|
// Delete a folder from disk.
|
|
// Called from delete button in shows page.
|
|
// setTimeot - since we do it the AJAX style.
|
|
var foldername = document.getElementById('lists').value;
|
|
if (foldername==""){return};
|
|
if (confirm('SURE?! Delete "' + foldername + '" and all it\'s data?')) {
|
|
fetch('/shows/' + foldername, { method: 'DELETE' });
|
|
setTimeout(function(){ document.location = '/shows'; }, 500);
|
|
}
|
|
} // delshow ended
|
|
|
|
function delRow(e) {
|
|
// console.log('Would delete', e);
|
|
if (!e){return};
|
|
document.getElementById('itemList').removeChild(e);
|
|
// save data
|
|
SaveNewSortOrder();
|
|
} // delRow ended
|
|
|
|
function duplicateRundown() {
|
|
// Duplicate a rundown
|
|
data={};
|
|
let itemName = document.getElementById('lists').value;
|
|
if (!itemName) {return}
|
|
data.filename=itemName + '.json';
|
|
data.foldname=document.getElementById("hidden_folder").value;
|
|
ajaxpost('/gc/duplicateRundown',data);
|
|
setTimeout(function(){ document.location = '/show/' + data.foldname; }, 500);
|
|
} // duplicateRundown ended
|
|
|
|
function duplicateRundownItem(rowitem) {
|
|
if (!rowitem){
|
|
rowitem = getFocusedRow();
|
|
}
|
|
|
|
// Collect data for the server to duplicating the item
|
|
data={};
|
|
data.command = "duplicateRundownItem";
|
|
data.sourceEpoch = rowitem.getAttribute('data-spx-epoch');
|
|
data.cloneEpoch = String(Date.now()); // create epoch and give this to server also
|
|
data.foldername = document.getElementById('foldername').value;
|
|
data.listname = document.getElementById('filebasename').value;
|
|
data.datafile = document.getElementById('datafile').value;
|
|
|
|
var newItem = rowitem.cloneNode(true);
|
|
newItem.style.opacity=0;
|
|
|
|
// update table id's for sorting to keep up. A bug found right after 1.0.12 was released.
|
|
updateFormIndexes()
|
|
rowitem.after(newItem);
|
|
|
|
let list = [
|
|
newItem.setAttribute('data-spx-epoch', data.cloneEpoch),
|
|
newItem.setAttribute('data-spx-onair', "false"),
|
|
newItem.querySelector('.copyid').setAttribute('onmouseover', 'tip("Duplicated item ' + data.cloneEpoch + '")'),
|
|
newItem.querySelector('.utilityOverlay').innerHTML="Reload page to update this duplicated item.",
|
|
newItem.querySelector('[data-spx-name="icon"]').classList.add('playFalse'),
|
|
newItem.querySelector('[data-spx-name="playbutton"]').classList.add('bg_green'),
|
|
newItem.querySelector('[data-spx-name="icon"]').classList.remove('playTrue'),
|
|
newItem.querySelector('[data-spx-name="playbutton"]').classList.remove('bg_red'),
|
|
newItem.querySelector('[data-spx-name="playbutton"]').textContent = newItem.querySelector('[data-spx-name="playbutton"]').getAttribute('data-spx-playtext'),
|
|
rowitem.classList.remove('inFocus'),
|
|
newItem.classList.add('inFocus'),
|
|
newItem.style.opacity=1
|
|
]
|
|
|
|
applyCommands(list);
|
|
working('Sending ' + data.command + ' request.');
|
|
ajaxpost('',data);
|
|
|
|
function applyCommands(list) {
|
|
// delay commands utility
|
|
let totalDelay = 0;
|
|
let stepDelay = 10;
|
|
list.forEach((item,index) => {
|
|
totalDelay += stepDelay;
|
|
setTimeout(function () { (item) }, totalDelay);
|
|
});
|
|
}
|
|
|
|
} // duplicateRundownItem ended
|
|
|
|
function edi() {
|
|
// Edit a datafile on disk.
|
|
// Called from edit button in lists page.
|
|
var filename = document.getElementById('lists').value;
|
|
document.location = '/shows/' + filename;
|
|
} // edi ended
|
|
|
|
function openRelpathFolder(itemrow) {
|
|
// added in 1.1.1 - Open a file for editing.
|
|
let fileRef = itemrow.querySelector("[id^='relpath']").value;
|
|
AJAXGET('/api/openFileFolder?file=' + fileRef);
|
|
} // openRelpathFolder ended
|
|
|
|
function openLightModePanel() {
|
|
// Open a light mode panel (in Controller only)
|
|
let newUrl = window.location.href + '/light';
|
|
window.open(newUrl, 'SPX-LITE', '_blank, width=680, height=850, scrollbars=yes, location=yes,status=yes');
|
|
}
|
|
|
|
function eps() {
|
|
// Opens the selected (or current) file in controller.
|
|
// Function works both in Edit and Lists -views.
|
|
// require ..... nothing
|
|
// returns ..... reloads the browser to another URL
|
|
let foldername = "";
|
|
let element = "";
|
|
element = document.getElementById("lists");
|
|
if (typeof (element) != 'undefined' && element != null) {
|
|
foldername = element.value;
|
|
}
|
|
|
|
element = document.getElementById("filebasename");
|
|
if (typeof (element) != 'undefined' && element != null) {
|
|
foldername = element.value;
|
|
}
|
|
if (foldername==""){return};
|
|
document.location = '/show/' + foldername;
|
|
} // cas ended
|
|
|
|
function exportItemAsCSV(rowItem) {
|
|
// export an item as a CSV file, which then can be
|
|
// filled in and imported back, so it will auto-generate
|
|
// a bunch of graphics.
|
|
// Added in 1.0.15
|
|
let data = {};
|
|
data.foldername = document.getElementById('foldername').value;
|
|
data.datafile = document.getElementById('filebasename').value;
|
|
data.itemID = rowItem.getAttribute('data-spx-epoch');
|
|
working('Generating CSV file to ASSETS/csv -folder.');
|
|
ajaxpost('/api/exportCSVfile',data);
|
|
showMessageSlider('CSV file generated to ASSETS/csv -folder.')
|
|
AJAXGET('/api/openFileFolder?openFolderOnly=csv');
|
|
|
|
} // exportItemAsCSV ended
|
|
|
|
function ModalOn(modalID) {
|
|
// Added in 1.1.4 - reset selections
|
|
var allItems = document.getElementsByClassName("filebrowser_file") || [];
|
|
Array.prototype.forEach.call(allItems, function(el) {
|
|
el.classList.remove('selectedFile')
|
|
});
|
|
|
|
if (document.getElementById('allToggle')) {
|
|
document.getElementById('allToggle').innerText=document.getElementById('allToggle').getAttribute('data-all-label');
|
|
}
|
|
|
|
document.getElementById(modalID).style.display = "block";
|
|
} //FileBrowserOn
|
|
|
|
function ModalOff(modalID) {
|
|
document.getElementById(modalID).style.display = "none";
|
|
} //FileBrowserOff
|
|
|
|
function openImportDialog() {
|
|
ModalOff('overlayTemplatePicker')
|
|
ModalOn('filebrowserModal')
|
|
// console.log('Opening File browser dialog');
|
|
} // openImportDialog
|
|
|
|
function filterProjects() {
|
|
// Added in 1.1.0 - will hide/show items in the allProjects list
|
|
var input, filterPhrase, projList, li, itemValue, i, txtValue;
|
|
input = document.getElementById('projectFilter');
|
|
filterPhrase = input.value.toUpperCase();
|
|
projList = document.getElementById("allProjects");
|
|
optionItems = projList.getElementsByTagName('option');
|
|
lists.innerHTML=''; // clear
|
|
for (i = 0; i < optionItems.length; i++) {
|
|
txtValue = optionItems[i].innerText;
|
|
let filters = filterPhrase.split(" ")
|
|
filters = filters.filter(f => f.length)
|
|
let shouldDisplay = true
|
|
filters.forEach(filterItem => {
|
|
shouldDisplay = shouldDisplay && txtValue.toUpperCase().includes(filterItem)
|
|
})
|
|
if (shouldDisplay || filters.length === 0) {
|
|
var opt = document.createElement('option');
|
|
opt.innerHTML = txtValue;
|
|
lists.appendChild(opt);
|
|
}
|
|
}
|
|
} // filterProjects ended
|
|
|
|
function focusRow(rowitemOrIndex) {
|
|
// will make TG item focused and will set masterbutton states.
|
|
// console.log('Focusing', typeof rowitemOrIndex, rowitemOrIndex);
|
|
|
|
// clear focus from all rows first
|
|
document.querySelectorAll('.itemrow').forEach(function (item, i) {
|
|
if (item.classList.contains("inFocus")) {
|
|
item.classList.remove('inFocus');
|
|
} else {
|
|
item.setAttribute('data-spx-previewing', 'false'); // added in 1.1.1
|
|
}
|
|
})
|
|
var index = 0; // for master buttons
|
|
|
|
let TargetElement = '';
|
|
if (typeof rowitemOrIndex == "number") {
|
|
// console.log('rowitemOrIndex was a number');
|
|
TargetElement = document.querySelectorAll('.itemrow')[rowitemOrIndex];
|
|
index = rowitemOrIndex;
|
|
}
|
|
|
|
if (typeof rowitemOrIndex == "object") {
|
|
TargetElement = rowitemOrIndex;
|
|
index = getIndexOfRowItem(rowitemOrIndex);
|
|
}
|
|
|
|
if (TargetElement){
|
|
TargetElement.classList.add('inFocus');
|
|
TargetElement.scrollIntoView({
|
|
behavior: "auto",
|
|
block: "nearest",
|
|
});
|
|
|
|
// Added in 1.1.0:
|
|
if (document.getElementById('previewMode').value==='selected') {
|
|
if (TargetElement.getAttribute('data-spx-previewing')!='true') {
|
|
TargetElement.setAttribute('data-spx-previewing', 'true'); // added in 1.1.1 to avoid double previewing
|
|
previewItem(); // preview selected item
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setMasterButtonStates(TargetElement, 'from focusRow');
|
|
} // focusRow ended
|
|
|
|
|
|
function getFocusedRow() {
|
|
// a utility to iterate DOM rows, returns focused element reference
|
|
return document.querySelectorAll('.inFocus')[0];
|
|
} // getFocusedRow ended
|
|
|
|
|
|
function getElementByEpoch(itemID) {
|
|
// get element by epoch id (added in 1.0.8)
|
|
let DomItemID;
|
|
for (let index = 0; index < document.querySelectorAll('.itemrow').length; index++) {
|
|
DomItemID = document.querySelectorAll('.itemrow')[index].getAttribute('data-spx-epoch');
|
|
if (DomItemID==itemID) {
|
|
return document.querySelectorAll('.itemrow')[index]
|
|
};
|
|
}
|
|
return false; // added in 1.2.0
|
|
} // getElementByEpoch ended
|
|
|
|
|
|
function getIndexOfRowItem(rowitem) {
|
|
// Utility, gets a row element and returns it's index
|
|
document.querySelectorAll('.itemrow').forEach(function (item, i) {
|
|
if (rowitem = item) {
|
|
return i;
|
|
}
|
|
})
|
|
} // getIndexOfRowItem ended
|
|
|
|
async function appendSpxApiKey (data) {
|
|
// Added in v1.3.2
|
|
// Adds API key to data object or a URL
|
|
// from localstorage or a URL parameter
|
|
|
|
// Get apikey from URL
|
|
queryString = window.location.search;
|
|
urlParams = new URLSearchParams(queryString);
|
|
addressKey = urlParams.get('apikey') || null;
|
|
|
|
// Get apikey from local storage
|
|
let storageKey = localStorage.getItem('spxApiKey') || null;
|
|
|
|
if (!addressKey && !storageKey) {
|
|
// No API key found, return original data
|
|
return data;
|
|
}
|
|
|
|
let apikey = storageKey; // default to storage key
|
|
if (addressKey) {
|
|
apikey = addressKey; // use address key if present
|
|
}
|
|
|
|
if (typeof data === 'object') {
|
|
// POST - so add API key to data
|
|
data.apikey = apikey;
|
|
return data; // return object with API key
|
|
} else {
|
|
// GET - so add API key to URL
|
|
let char = data.includes('?') ? '&' : '?';
|
|
data += char + 'apikey=' + apikey;
|
|
return data; // return URL with API key
|
|
}
|
|
} // appendSpxApiKey
|
|
|
|
function getDomIndexByElementId(ElementId) {
|
|
// A utility to iterate all DOM items and return dom index of given ElementId
|
|
// request = ElementID
|
|
// returns = dom index of the given ElementID
|
|
|
|
for (let DomNro = 0; DomNro < document.querySelectorAll('.itemrow').length; DomNro++) {
|
|
if (document.querySelectorAll('.itemrow')[DomNro].id=='playlistitem' + String(ElementId))
|
|
{
|
|
// console.log('Found DomIndex ' + DomNro + ' for ElementID ' + String(ElementId) + '.');
|
|
return DomNro
|
|
};
|
|
}
|
|
} // getDomIndexByElementId ended
|
|
|
|
function getLayerFromProfile(buttonName) {
|
|
// Make JSON from profiledata string from a hidden field and search it...
|
|
// console.log('Searching layer for ' + buttonName);
|
|
// This is a utility which is called bu Vue when checking button states.
|
|
let profiledata = JSON.parse(document.getElementById('profiledata').value);
|
|
let prfName = document.getElementById('profname').innerText.toUpperCase();
|
|
for (var i = 0; i < profiledata.profiles.length; i++) {
|
|
let curName = profiledata.profiles[i].name.toUpperCase();
|
|
if (curName == prfName) {
|
|
let SRV = profiledata.profiles[i].templates[buttonName].server;
|
|
let CHA = profiledata.profiles[i].templates[buttonName].channel;
|
|
let LAY = profiledata.profiles[i].templates[buttonName].layer;
|
|
let CCG = SRV + " " + CHA + " " + LAY;
|
|
// console.log("Render layer of [" + buttonName + "] in profile [" + prfName + "] is [" + CCG + "].");
|
|
return CCG;
|
|
}
|
|
}
|
|
} // getLayerFromProfile ended
|
|
|
|
function getMessages(curVerInfo) {
|
|
// Poll server for upgrade info. ------------------------------
|
|
// Requires CORS in the route.
|
|
// Moved from view-home.
|
|
//
|
|
// PLEASE NOTE: This will be replaced with improved mechanism
|
|
// which will report usage stats also and will
|
|
// notify user with a small icon to reveal the
|
|
// promo within controller UI.
|
|
// This will happen in 1.1.x.
|
|
//
|
|
// All passed data is anonymous and non-identifiable
|
|
// -------------------------------------------------------------
|
|
|
|
|
|
let versionStr = curVerInfo.split('v=')[1].split('&')[0].trim();
|
|
// console.log('---------- getMessages [' + versionStr + ']', curVerInfo);
|
|
|
|
localStorage.removeItem('SPX-GC-NewVersion');
|
|
document.getElementById('upgradeinfo').style.display="none";
|
|
document.getElementById('messageinfo').style.display="none";
|
|
var url = (`https://www.smartpx.fi/gc/messageservice/?` + curVerInfo );
|
|
fetch(url)
|
|
.then((res) => res.json())
|
|
.then((messages) => {
|
|
let currntVer = document.getElementById('footerVerDisplay').innerText;
|
|
let latestVer = messages.latest.vers;
|
|
let dbggreet = messages.dbggreet || "";
|
|
|
|
// this has been broken until 1.0.11
|
|
let debugNotifications = false;
|
|
if ( document.getElementById('greeting') && document.getElementById('greeting').innerText!='' ) {
|
|
if ( dbggreet == document.getElementById('greeting').innerText ) {
|
|
// if config.greeting == message.greeting then debug it!
|
|
debugNotifications = true;
|
|
}
|
|
}
|
|
|
|
if (messages.latest.vers && versInt(latestVer) > versInt(currntVer) || debugNotifications ) {
|
|
// Yes, there is new version available
|
|
localStorage.setItem('SPX-GC-NewVersion',latestVer);
|
|
document.getElementById('upgradeinfo').style.display="block";
|
|
document.getElementById('upgrade_date').innerText=messages.latest.date;
|
|
document.getElementById('upgrade_head').innerText=messages.latest.head;
|
|
document.getElementById('upgrade_body').innerText=messages.latest.body;
|
|
document.getElementById('upgrade_call').innerText=messages.latest.call;
|
|
document.getElementById('upgrade_link').href=messages.latest.href;
|
|
document.getElementById('upgrade_link').innerText=messages.latest.link;
|
|
}
|
|
|
|
if (messages.notification.type && messages.notification.type!="none" || debugNotifications ){
|
|
// Yes, there is something else to announce
|
|
document.getElementById('messageinfo').style.display="block";
|
|
document.getElementById('messageinfo').classList="message-" + messages.notification.type;
|
|
document.getElementById('messagelink').classList="message-" + messages.notification.type;
|
|
document.getElementById('message_date').innerText=messages.notification.date;
|
|
document.getElementById('message_head').innerText=messages.notification.head;
|
|
document.getElementById('message_body').innerText=messages.notification.body;
|
|
document.getElementById('message_call').innerText=messages.notification.call;
|
|
document.getElementById('message_link').href=messages.notification.href;
|
|
document.getElementById('message_link').innerText=messages.notification.link;
|
|
}
|
|
|
|
return // Check if there is a promo for this version
|
|
|
|
// Added in 1.1.2
|
|
// versionStr = "1.0.0"; // DEBUG## with specific version number
|
|
if (messages.homepagepromo[versionStr] ) {
|
|
// console.log('Promo available for v.' + versionStr, messages.homepagepromo[versionStr]);
|
|
document.getElementById('homepagepromolink').href=messages.homepagepromo[versionStr].link;
|
|
document.getElementById('homepagepromopict').src=messages.homepagepromo[versionStr].pict;
|
|
document.getElementById('homepagepromopict').title=messages.homepagepromo[versionStr].titl;
|
|
} else {
|
|
// Check if there is a default promo
|
|
if (messages.homepagepromo && messages.homepagepromo.default && messages.homepagepromo.default.active ){
|
|
// console.log('Using active default promo.', messages.homepagepromo.default);
|
|
document.getElementById('homepagepromolink').href=messages.homepagepromo.default.link;
|
|
document.getElementById('homepagepromopict').src=messages.homepagepromo.default.pict;
|
|
document.getElementById('homepagepromopict').title=messages.homepagepromo.default.titl;
|
|
}
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('SPX error in getMessages().',error)
|
|
});
|
|
} // getMessages ended
|
|
|
|
function getMessages2(or) {
|
|
// This is called from heartbeat after a day change and will
|
|
// post anonymous usage data and may get potential notification
|
|
// data back to be displayed in the SPX user interface.
|
|
// This function is in early stage in v 1.1.0 and will be
|
|
// fully implemented in future versions.
|
|
ajaxpost('/api/heartbeat', {data:or});
|
|
} // getMessages2 ended
|
|
|
|
function handleRendererPopups(data) {
|
|
// open / close renderer popups
|
|
// data: {type:preview}
|
|
// Save to config.json general.renderer as "normal" (embedded inline renderer) or "popup" (floating window)
|
|
|
|
let URL;
|
|
let socketData = {};
|
|
var w = 1280;
|
|
var h = 720;
|
|
var left = (screen.width/2)-(w/2);
|
|
var top = (screen.height/2)-(h/2);
|
|
var OPT = '_blank,toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left;
|
|
|
|
if (data.type==='preview') { URL = '/renderwindow/preview'; winRef = 'SPXpreviewWindow'}
|
|
if (data.type==='program') { URL = '/renderwindow/program'; winRef = 'SPXprogramWindow'}
|
|
|
|
if (data.command == 'open') {
|
|
// open the popup
|
|
window.open(URL, winRef, OPT);
|
|
}
|
|
|
|
if (data.command == 'close') {
|
|
// send message request to close the popup
|
|
// send message to close the window without prompts (closeprogram closepreview)
|
|
socketData.spxcmd = 'close' + data.type;
|
|
socket.emit('SPXWebRendererMessage', socketData);
|
|
}
|
|
|
|
if (data.type == 'program') {
|
|
toggleNormalRenderer(data.command)
|
|
}
|
|
} // handleRendererPopups ended
|
|
|
|
function help(section) {
|
|
// Help is massively a WORK-IN-PROGRESS thing!
|
|
// Wanna help? Let us know.
|
|
//
|
|
// Open help feature. This is a central location to
|
|
// handle help links. The 'section' argument is optional
|
|
// and can redirect to desired pages/bookmarks...
|
|
//
|
|
// request ..... section or <empty>
|
|
// returns ..... opens a tab with help content
|
|
|
|
let HELP_ROOT = "https://docs.spxgraphics.com/"
|
|
let HELP_PAGE = ""
|
|
section = section.toUpperCase()
|
|
|
|
switch (section) {
|
|
case 'INTRO':
|
|
HELP_PAGE = 'Welcome+to+SPX+Docs';
|
|
break;
|
|
case 'PROJECTS':
|
|
HELP_PAGE = 'Documentation/Server/Content+Management#Projects';
|
|
break;
|
|
case 'PLAYLISTS':
|
|
HELP_PAGE = 'Documentation/Server/Content+Management#Rundowns';
|
|
break;
|
|
case 'CONFIG':
|
|
HELP_PAGE = 'Documentation/Graphics+Controller/Application+Configuration';
|
|
break;
|
|
case 'WEBPLAYOUT':
|
|
HELP_PAGE = 'Documentation/Renderer/Overview';
|
|
break;
|
|
case 'CASPARCG':
|
|
HELP_PAGE = 'Documentation/Renderer/Workflows/CasparCG+-+AMCP';
|
|
break;
|
|
case 'GLOBALEXTRAS':
|
|
HELP_PAGE = 'Documentation/Graphics+Controller/Plugins+%26+Extensions#Global+Extras+vs+Plugins';
|
|
break;
|
|
case 'PROJECTGENERAL':
|
|
HELP_PAGE = 'Documentation/Graphics+Controller/Project+Settings#Template+Configuration';
|
|
break;
|
|
case 'PROJECTTEMPLATES':
|
|
HELP_PAGE = 'Documentation/Graphics+Controller/Project+Settings';
|
|
break;
|
|
case 'PROJECTVARIABLES':
|
|
HELP_PAGE = 'article/help-project-variables';
|
|
break;
|
|
case 'PROJECTEXTRAS':
|
|
HELP_PAGE = 'Documentation/Graphics+Controller/Project+Settings#Project+Extras+(showExtras)';
|
|
break;
|
|
case 'CONTROLLER':
|
|
HELP_PAGE = 'Documentation/Control+Interfaces/External+Control';
|
|
break;
|
|
case 'CSV':
|
|
HELP_PAGE = 'Guides/Tutorials/how+to+use+csv+files';
|
|
break;
|
|
case 'API':
|
|
HELP_PAGE = 'Documentation/Control+Interfaces/REST/Overview+of+SPX+API';
|
|
break;
|
|
case 'ITEM-DETAILS':
|
|
HELP_PAGE = 'Documentation/Graphics+Controller/Overview#Playout+details';
|
|
break;
|
|
case 'SEVERAL':
|
|
HELP_PAGE = 'Documentation/Graphics+Controller/Overview#Several+SPX+controllers';
|
|
break;
|
|
case 'LIGHTMODE':
|
|
HELP_PAGE = 'article/light-mode';
|
|
break;
|
|
case 'MOS':
|
|
HELP_PAGE = 'Documentation/Server/Integration/MOS';
|
|
break;
|
|
case 'OLDBROWSERDETECTED':
|
|
HELP_PAGE = 'article/old-browser-detected';
|
|
break;
|
|
case 'LICENSING':
|
|
HELP_PAGE = 'Documentation/Server/License';
|
|
break;
|
|
case 'CALLUPID':
|
|
HELP_PAGE = 'article/callupid';
|
|
break;
|
|
case 'THUMBNAILGENERATION':
|
|
HELP_PAGE = 'article/thumbnail-generation';
|
|
break;
|
|
case 'OGRAF':
|
|
HELP_PAGE = 'Documentation/Graphic+Templates/Formats/OGraf';
|
|
break;
|
|
default:
|
|
HELP_PAGE = 'Welcome+to+SPX+Docs/';
|
|
break;
|
|
}
|
|
|
|
let FULL_URL = HELP_ROOT + HELP_PAGE;
|
|
window.open(FULL_URL, 'GCHELP', '_blank,width=980,height=800,scrollbars=yes,location=yes,status=yes');
|
|
} // help ended
|
|
|
|
function moveFocus(nro) {
|
|
// Move highlighted row up/down with keyboard commands
|
|
// Request .... nro = negative (-1) or positive (1)
|
|
// Returns .... nothing, just set focus
|
|
let itemCount = document.querySelectorAll('.itemrow').length;
|
|
let curIndex=0;
|
|
for (let item = 0; item < itemCount; item++) {
|
|
if (document.querySelectorAll('.itemrow')[item].classList.contains('inFocus')) {
|
|
curIndex=item; }
|
|
}
|
|
|
|
if (nro > 0 ){
|
|
// positive, move downwards
|
|
newIndex=curIndex+1;
|
|
//if (newIndex>itemCount-1){newIndex=0}; // go back to top
|
|
if (newIndex>itemCount-1){newIndex=itemCount-1}; // stay at the bottom
|
|
focusRow(newIndex);
|
|
}
|
|
if (nro < 0 ){
|
|
// negative, move upwards
|
|
newIndex=curIndex-1;
|
|
//if (newIndex<0){newIndex=itemCount-1}; // go to end of list
|
|
if (newIndex<0){newIndex=0}; // stay at the top
|
|
focusRow(newIndex);
|
|
}
|
|
} // moveFocus ended
|
|
|
|
function nextItem(itemrow='') {
|
|
// Next / continue command.
|
|
|
|
if (!itemrow) {
|
|
// itemrow not given, get focused row
|
|
itemrow = getFocusedRow();
|
|
}
|
|
|
|
data = {};
|
|
data.datafile = document.getElementById('datafile').value;
|
|
data.epoch = itemrow.getAttribute('data-spx-epoch');
|
|
|
|
// decrease steps left
|
|
var stepsleft= parseInt(itemrow.getAttribute('data-spx-stepsleft'))-1;
|
|
itemrow.setAttribute('data-spx-stepsleft',stepsleft);
|
|
if (stepsleft < 1) {
|
|
// console.log('No more steps (' + stepsleft + ') left, stopping item.');
|
|
// This should go somewhere else - because of DRY.
|
|
// This duplicates some code from playItem
|
|
itemrow.setAttribute('data-spx-onair', false);
|
|
itemrow.setAttribute('data-spx-stepsleft',itemrow.querySelector('input[name="RundownItem[steps]"]').value); // reset counter
|
|
let uselessReturnValue = setItemButtonStates(itemrow, forcedCommand='stop');
|
|
document.getElementById('MasterCONTINUE').classList.add('disabled');
|
|
data.command = 'stop';
|
|
setMasterButtonStates(itemrow);
|
|
} else {
|
|
// console.log('Steps left: ' + stepsleft + ', continuing item...');
|
|
data.command = 'next';
|
|
}
|
|
|
|
working('Sending ' + data.command + ' request.');
|
|
ajaxpost('/gc/playout',data);
|
|
|
|
//### DEBUG
|
|
return;
|
|
|
|
// Check for function_onNext event (added in 1.1.4):
|
|
let onNextField = itemrow.querySelector('[name="RundownItem[function_onNext]"]');
|
|
// console.log('onNextField: ', data.command, onNextField);
|
|
if (onNextField && onNextField.value!="") {
|
|
// console.log('onNextField: ' + onNextField.value);
|
|
let onNextCommand = onNextField.value.split('|') || [];
|
|
// console.log('[' + String(new Date().toLocaleTimeString()) + '] ' + 'NEXT event: ' + onNextCommand);
|
|
let FunctionName = String(onNextCommand[0] || 'noFunctionName').trim();
|
|
let FunctionArgs = String(onNextCommand[1] || 'NoArgs').trim();
|
|
let ExecuteDelay = String(onNextCommand[2] || '100').trim();
|
|
let ConditFField = String(onNextCommand[3] || 'XXX').trim();
|
|
let conditionVal = null;
|
|
let eventCondtFF = itemrow.querySelector('[data-update=' + ConditFField + ']');
|
|
if (eventCondtFF) { conditionVal = eventCondtFF.value; }
|
|
setTimeout(function () {
|
|
// console.log('FunctionName:' + FunctionName);
|
|
window[FunctionName]([FunctionArgs, conditionVal, data.epoch]);
|
|
}, parseInt(ExecuteDelay));
|
|
heartbeat(314); // identifier
|
|
}
|
|
|
|
|
|
} // nextItem
|
|
|
|
function previewItem(itemrow='') {
|
|
// Added in 1.1.0
|
|
// console.log('Itemrow: ', itemrow);
|
|
if (!itemrow) { itemrow = getFocusedRow(); }
|
|
if (!itemrow) { /* console.log('No active rows, skip command.');*/ return;}
|
|
itemrow.setAttribute('data-spx-previewing', 'true'); // added in 1.1.1 to avoid double previewing. See also focusRow()
|
|
data = {};
|
|
data.datafile = document.getElementById('datafile').value;
|
|
data.epoch = itemrow.getAttribute('data-spx-epoch') || 0;
|
|
data.command = 'preview'; // added in 1.1.0
|
|
working('Sending ' + data.command + ' request.');
|
|
ajaxpost('/gc/playout',data);
|
|
heartbeat(308); // identifier
|
|
|
|
|
|
} // previewItem
|
|
|
|
function projectSettings() {
|
|
// Open project settings page from rundown list page
|
|
document.location.href += '/config' ;
|
|
} // projectSettings
|
|
|
|
function playItem(itemrow='', forcedCommand='', preventPlayout=false) {
|
|
// Play item toggle.
|
|
//
|
|
// request ..... itemrow
|
|
// forcedCommand is (for example) to stop item when continue steps are done
|
|
// returns ..... ajax response message to GUI
|
|
//
|
|
// Will send data object with an index and playlist filename
|
|
// and playout handler will parse required data and so on.
|
|
//
|
|
// FIXME: Bug found 2023-11-07 Step counter in DOM needs to be reset on Play
|
|
//
|
|
// v1.3.2 - added "preventPlayout" flag. This is used in the new "updateRundownItem" feature
|
|
// in directplayout API endpoint is present.
|
|
|
|
if (!itemrow) { itemrow = getFocusedRow(); }
|
|
if (!itemrow) { return; }
|
|
|
|
data = {};
|
|
data.datafile = document.getElementById('datafile').value;
|
|
data.epoch = itemrow.getAttribute('data-spx-epoch') || 0;
|
|
data.command = setItemButtonStates(itemrow, forcedCommand); // update buttons and return command (play/stop/playonce). ForcedCommand (stop) overrides.
|
|
setMasterButtonStates(itemrow, 'from playItem'); // update master button UI
|
|
|
|
if (preventPlayout) {
|
|
console.log('Prevent playout flag is set, skipping playout.');
|
|
} else {
|
|
working('Sending ' + data.command + ' request.');
|
|
ajaxpost('/gc/playout',data);
|
|
}
|
|
|
|
// Stats
|
|
if (data.command == 'play' || data.command=="playonce") { heartbeat(302) }
|
|
if (data.command == 'continue' ) { heartbeat(304) }
|
|
if (data.command == 'stop' ) { heartbeat(306) }
|
|
|
|
// playonce (for out type=none) command acts on the server, but the state will not be saved to JSON
|
|
let isPlay = false;
|
|
if (data.command=="play" || data.command=="playonce") {
|
|
isPlay = true;
|
|
|
|
// Added in 1.1.0 FIXME: This has some issues, previewing wrong items etc...
|
|
if (document.getElementById('previewMode').value==='next') {
|
|
console.log('Preview-next-on-play IS WIP and IS CURRENTY DISABLED');
|
|
|
|
// TODO: Enable this:
|
|
// let nextRundownItem = getFocusedRow().nextElementSibling;
|
|
// console.log('Next elementID ' + nextRundownItem.id, nextRundownItem);
|
|
// previewItem(nextRundownItem);
|
|
}
|
|
|
|
}
|
|
|
|
// Check for function_onPlay event (improved in 1.1.4):
|
|
let onPlayField = itemrow.querySelector('[name="RundownItem[function_onPlay]"]');
|
|
if (isPlay && onPlayField && onPlayField.value!="") {
|
|
|
|
// TODO: This needs more work! =============================
|
|
// Now this expects an array of 4 values, but function calls
|
|
// can have 0 or several arguments...
|
|
// ========================================================
|
|
|
|
// Changed in v1.1.4 (no other server changes, just "spx_gc.js")
|
|
// Examples of valid "function_onPlay/nStop" values:
|
|
//
|
|
// myCustomMethod | {'foo': 'bar'} | 500 | f1
|
|
// myCustomMethod | 'foo' | 500 | f1
|
|
//
|
|
// 1st: Function name (no parenthesis),
|
|
// 2nd: an optional JSON object or a string argument,
|
|
// 3rd: execution delay in ms.
|
|
// 4th: one of the f-fields (value can be used as a condition in the function)
|
|
|
|
|
|
let onPlayCommand = onPlayField.value.split('|') || [];
|
|
let FunctionName = String(onPlayCommand[0] || 'noFunctionName').trim();
|
|
let FunctionArgs = String(onPlayCommand[1] || 'NoArgs').trim();
|
|
let ExecuteDelay = String(onPlayCommand[2] || '100').trim();
|
|
let ConditFField = String(onPlayCommand[3] || 'XXX').trim();
|
|
let conditionVal = null;
|
|
let eventCondtFF = itemrow.querySelector('[data-update=' + ConditFField + ']');
|
|
|
|
// Improved in 1.3.0 - now supports also SELECT and CHECKBOX fields
|
|
if (eventCondtFF) {
|
|
switch (eventCondtFF.tagName) {
|
|
case 'SELECT':
|
|
conditionVal = eventCondtFF.options[eventCondtFF.selectedIndex].value;
|
|
break;
|
|
|
|
case 'INPUT':
|
|
if (eventCondtFF.type == 'checkbox') {
|
|
conditionVal = eventCondtFF.checked;
|
|
} else {
|
|
conditionVal = eventCondtFF.value;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
conditionVal = eventCondtFF.value;
|
|
}
|
|
}
|
|
|
|
setTimeout(function () {
|
|
if (window[FunctionName]) {
|
|
window[FunctionName]([FunctionArgs, conditionVal, data.epoch]); // Added data.epoch in 1.1.4
|
|
} else {
|
|
showMessageSlider('Function "' + FunctionName + '" not found in current custom functions library file.', 'warn');
|
|
}
|
|
}, parseInt(ExecuteDelay));
|
|
heartbeat(310); // identifier
|
|
}
|
|
|
|
// Check for function_onStop event (improved in 1.1.4):
|
|
let onStopField = itemrow.querySelector('[name="RundownItem[function_onStop]"]');
|
|
if (data.command=="stop" && onStopField && onStopField.value!="") {
|
|
// Changed in v1.1.4 (no other server changes, just "spx_gc.js")
|
|
let onStopCommand = onStopField.value.split('|') || [];
|
|
// console.log('[' + String(new Date().toLocaleTimeString()) + '] ' + 'STOP event: ' + onStopCommand);
|
|
let FunctionName = String(onStopCommand[0] || 'noFunctionName').trim();
|
|
let FunctionArgs = String(onStopCommand[1] || 'NoArgs').trim();
|
|
let ExecuteDelay = String(onStopCommand[2] || '100').trim();
|
|
let ConditFField = String(onStopCommand[3] || 'XXX').trim();
|
|
let conditionVal = null;
|
|
let eventCondtFF = itemrow.querySelector('[data-update=' + ConditFField + ']');
|
|
if (eventCondtFF) { conditionVal = eventCondtFF.value; }
|
|
setTimeout(function () {
|
|
// console.log('FunctionName:' + FunctionName);
|
|
window[FunctionName]([FunctionArgs, conditionVal, data.epoch]);
|
|
}, parseInt(ExecuteDelay));
|
|
heartbeat(312); // identifier
|
|
}
|
|
|
|
// Remember, onNext is handled in nextItem();
|
|
|
|
// auto-out trigger UI update (FIXME: verify auto-out works over direct API commands?!)
|
|
let TimeoutAsString = itemrow.querySelector('[name="RundownItem[out]"]').value;
|
|
// console.log('TimeoutAsString: ', TimeoutAsString);
|
|
|
|
if (!isNaN(TimeoutAsString)) {
|
|
// value is numerical, so
|
|
// console.log('TimeoutAsString is a number, so it is a timeout value.');
|
|
if (data.command=="play" ) {
|
|
// Changed in 1.0.9, failed in 1.0.8.
|
|
// 1.0.12: Fixed looping issue found by Koen.
|
|
let TimeoutAsInt = parseInt(TimeoutAsString);
|
|
// console.log('Will stop playlistitem ' + itemrow.getAttribute('data-spx-epoch') + ' in ' + TimeoutAsInt + 'ms...');
|
|
var AutoOutTimerID = setTimeout(function () {
|
|
if (itemrow.getAttribute('data-spx-onair')==='true'){
|
|
playItem(itemrow, 'stop');
|
|
}
|
|
itemrow.querySelector('[data-spx-name="icon"]').classList.remove('playAuto');
|
|
}, TimeoutAsInt);
|
|
itemrow.setAttribute('data-spx-timerid', AutoOutTimerID);
|
|
itemrow.querySelector('[data-spx-name="icon"]').classList.add('playAuto');
|
|
} else {
|
|
CancelOutTimerIfRunning(itemrow);
|
|
}
|
|
}
|
|
|
|
} // playItem
|
|
|
|
function playItemLocal(itemID) {
|
|
// Just play an item to the local Controller preview
|
|
// console.log('Sending playItemLocal request (spx-gc) ' + itemID + ' in 5 secs...');
|
|
data = {};
|
|
data.datafile = document.getElementById('datafile').value;
|
|
data.epoch = itemID || 0;
|
|
data.command = 'autoPlayLocal';
|
|
ajaxpost('/gc/playout',data);
|
|
heartbeat(308)
|
|
} // playItemLocal
|
|
|
|
function renameRundown() {
|
|
// Rename an existing rundown
|
|
// Improved in 1.3.0 with validation
|
|
var filename = document.getElementById('lists').value;
|
|
if (!filename) {return}
|
|
var foldname = document.getElementById("hidden_folder").value;
|
|
|
|
while(true){
|
|
newname = prompt("Rename rundown to?", filename).trim();
|
|
if ( isValid(newname) ) {
|
|
break;
|
|
} else {
|
|
alert("Please enter a valid file name, no special characters.");
|
|
}
|
|
}
|
|
if (newname != null && newname != "") {
|
|
data={};
|
|
data.orgname = filename + '.json';
|
|
data.newname = newname;
|
|
data.foldnam = foldname;
|
|
ajaxpost('/gc/renameRundown',data);
|
|
setTimeout(function(){ document.location = '/show/' + foldname; }, 300);
|
|
}
|
|
|
|
} // renameRundown ended
|
|
|
|
function versInt(semver){
|
|
// Returns a numeric value representing "1.0.0" formatted semantic version string.
|
|
// This works as long as max value of each field is 99!
|
|
// Moved from view-home in 1.1.0
|
|
let parts = semver.split(".");
|
|
let MajorInt = parseInt(parts[0].trim())*100000
|
|
let MinorInt = parseInt(parts[1].trim())*1000
|
|
let PatchInt = parseInt(parts[2].trim())
|
|
let versInt = (MajorInt + MinorInt + PatchInt);
|
|
// console.log('Semver ' + semver + ' = versInt ' + versInt);
|
|
return versInt
|
|
}
|
|
|
|
function setItemButtonStates(itemrow, forcedCommand=''){
|
|
// Utility function which will toggle play indicators in
|
|
// collapsed AND expanded views based on calling element's
|
|
// current state.
|
|
|
|
// forcedCommand is used to forcefully stop an item, which steps has run
|
|
// out after continue is done...
|
|
|
|
// v.1.0.8 also forced 'play' is being used by API v1 to force
|
|
// play only, not just toggle..
|
|
|
|
//
|
|
// Requests ..... playlistitem's eleIndex nro
|
|
// Response ..... 'play' or 'stop' state after toggle
|
|
//
|
|
|
|
if ( itemrow.querySelector('[name="RundownItem[out]"]').value=="none" ) {
|
|
// console.log('NONE out type in this graphic. Do not set the buttons in any way and return with playonce.');
|
|
if (forcedCommand=='stop') {
|
|
return 'stop'; // Added in v 1.1.2 to prevent playing bumper at "Stop All"
|
|
}
|
|
return 'playonce';
|
|
}
|
|
|
|
let CommandToExecute = 'play' // default action
|
|
let EXPANDEDPLAY = itemrow.querySelector('[data-spx-name="playbutton"]');
|
|
let rows = document.querySelectorAll('.itemrow');
|
|
|
|
|
|
if (itemrow.getAttribute('data-spx-onair')=="true" || forcedCommand=='stop') {
|
|
// so, we are playing and needs to stop
|
|
CommandToExecute = 'stop';
|
|
|
|
// Reset current step counter. Added in 1.3.3
|
|
itemrow.setAttribute('data-spx-stepsleft',itemrow.querySelector('input[name="RundownItem[steps]"]').value);
|
|
}
|
|
|
|
if (itemrow.getAttribute('data-spx-onair')=="true" && forcedCommand=='play') {
|
|
// so, we are playing and needs to play (API buttons for example)
|
|
CommandToExecute = 'play';
|
|
}
|
|
|
|
if ( CommandToExecute == 'stop' ) {
|
|
// Convert button to PLAY button and execute stop command
|
|
// console.log('was playing. so send STOP and make button PLAY');
|
|
itemrow.setAttribute('data-spx-onair','false');
|
|
itemrow.querySelector('[name="RundownItem[onair]"]').value='false';
|
|
itemrow.querySelector('[data-spx-name="icon"]').classList.remove('playTrue');
|
|
itemrow.querySelector('[data-spx-name="icon"]').classList.remove('playAuto');
|
|
itemrow.querySelector('[data-spx-name="icon"]').classList.add('playFalse');
|
|
EXPANDEDPLAY.innerText = EXPANDEDPLAY.getAttribute('data-spx-playtext');
|
|
EXPANDEDPLAY.classList.remove('bg_red');
|
|
EXPANDEDPLAY.classList.add('bg_green');
|
|
}
|
|
|
|
if ( CommandToExecute == 'play' ) {
|
|
// Convert button to STOP button and execute play command
|
|
// first we need to dim all other elements which are playing on same output channel / layer
|
|
// console.log('was stopped. so send PLAY and make button STOP');
|
|
let PlayoutConfig = itemrow.querySelector('[name="RundownItem[webplayout]"').value;
|
|
rows.forEach(function (item, index) {
|
|
let CurrentConfig = item.querySelector('[name="RundownItem[webplayout]"')?.value||0;
|
|
if (item.getAttribute('data-spx-onair')=="true" && CurrentConfig==PlayoutConfig ) {
|
|
console.log('Dom item ' + index + ' is the same, so dim it');
|
|
item.setAttribute('data-spx-onair','false');
|
|
item.querySelector('[name="RundownItem[onair]"]').value='false';
|
|
item.querySelector('[data-spx-name="icon"]').classList.remove('playTrue');
|
|
item.querySelector('[data-spx-name="icon"]').classList.add('playFalse');
|
|
CancelOutTimerIfRunning(itemrow);
|
|
curExpandedPlay = item.querySelector('[data-spx-name="playbutton"]')
|
|
curExpandedPlay.innerText = curExpandedPlay.getAttribute('data-spx-playtext');
|
|
curExpandedPlay.classList.remove('bg_red');
|
|
curExpandedPlay.classList.add('bg_green');
|
|
}
|
|
})
|
|
EXPANDEDPLAY.innerText = EXPANDEDPLAY.getAttribute('data-spx-stoptext');
|
|
EXPANDEDPLAY.classList.remove('bg_green');
|
|
EXPANDEDPLAY.classList.add('bg_red');
|
|
itemrow.setAttribute('data-spx-onair','true');
|
|
itemrow.querySelector('[name="RundownItem[onair]"]').value='true';
|
|
itemrow.querySelector('[data-spx-name="icon"]').classList.remove('playFalse');
|
|
itemrow.querySelector('[data-spx-name="icon"]').classList.add('playTrue');
|
|
// // reset update buttons
|
|
// itemrow.setAttribute('data-spx-changed','false');
|
|
// itemrow.querySelector('[data-spx-name="updatebutton"]').classList.add('disabled');
|
|
|
|
}
|
|
|
|
if ( ife('identifier').value=="controller" ) {
|
|
// finally show or hide delete buttons on all items
|
|
rows.forEach(function (item, index) {
|
|
if (item.getAttribute('data-spx-onair')=="true" ) {
|
|
// console.log('Hide delete buttons of ' + item.id);
|
|
item.querySelector('[data-spx-name="deletesmall"]').style.visibility="hidden" || (console.log('a'));
|
|
item.querySelector('[data-spx-name="deletelarge"]').style.visibility="hidden" || (console.log('b'));
|
|
} else {
|
|
// console.log('Show delete buttons of ' + item.id);
|
|
item.querySelector('[data-spx-name="deletesmall"]').style.visibility="visible" || (console.log('c'));
|
|
item.querySelector('[data-spx-name="deletelarge"]').style.visibility="visible" || (console.log('d'));
|
|
}
|
|
})
|
|
}
|
|
return CommandToExecute
|
|
} // setItemButtonStates ended
|
|
|
|
function post(path, params, method) {
|
|
// Creates a hidden form in DOM, populates it and posts it to server.
|
|
// Feels dumb, but works, so what the heck.
|
|
// This is ONLY used when the POST command can yield a new page,
|
|
// for in-page AJAX calls ajaxpost() should be used.
|
|
const form = document.createElement('form');
|
|
form.method = method;
|
|
form.action = path;
|
|
for (const key in params) {
|
|
if (params.hasOwnProperty(key)) {
|
|
const hiddenField = document.createElement('input');
|
|
hiddenField.type = 'hidden';
|
|
hiddenField.name = key;
|
|
hiddenField.value = params[key];
|
|
form.appendChild(hiddenField);
|
|
}
|
|
}
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
} // post ended
|
|
|
|
function resizeInput() {
|
|
// changes headline1 width
|
|
this.style.width = (this.value.length + 1.2) + "ch";
|
|
} // resizeInput ended
|
|
|
|
async function revealItemID(button) {
|
|
let item = button.closest('.itemrow')
|
|
let ID = item.getAttribute('data-spx-epoch');
|
|
copyText(ID)
|
|
// alert("Item ID " + ID + " was copied to clipboard.");
|
|
var newID = prompt("Item ID " + ID + " was copied to clipboard.\nYou can change the ID here:", ID);
|
|
if (newID != null && newID != ID) {
|
|
// alert("New ID " + newID)
|
|
let file = document.getElementById('datafile').value;
|
|
let URL = `/api/v1/changeItemID?rundownfile=${file}&ID=${ID}&newID=${newID}`
|
|
let IDchanged
|
|
|
|
axios.get(URL)
|
|
.then(function (response) {
|
|
showMessageSlider('Item renamed to ' + newID + '. Reload recommended.')
|
|
item.setAttribute('data-spx-epoch', newID);
|
|
item.querySelector('.copyid').setAttribute('onmouseover', `tip('Copy ID ${newID}')`);
|
|
})
|
|
.catch(function (error) {
|
|
showMessageSlider('Unable to rename. Use unique ID and try again.', 'error')
|
|
});
|
|
|
|
|
|
// if (IDchanged) {
|
|
// // success
|
|
// item.setAttribute('data-spx-epoch', newID);
|
|
// } else {
|
|
// alert("Unable to change ID to " + newID + " (" + IDchanged + ").\nMake sure the ID is unique and try again.");
|
|
// }
|
|
} else {
|
|
// alert("Same ID " + newID)
|
|
}
|
|
} // revealItemID
|
|
|
|
async function revealItemTiming(button) {
|
|
let curValue = button.textContent;
|
|
let ID = button.closest('.itemrow').getAttribute('data-spx-epoch');
|
|
var newValue = prompt("You can change timing here. Use 'manual', 'none' or millisecond values (1000=1s):", curValue);
|
|
if (newValue != null && newValue != curValue) {
|
|
let file = document.getElementById('datafile').value;
|
|
let URL = `/api/v1/changeItemData?rundownfile=${file}&ID=${ID}&key=out&newValue=${newValue}`
|
|
axios.get(URL)
|
|
.then(function (response) {
|
|
showMessageSlider('Item out mode changed to ' + newValue)
|
|
button.textContent = newValue;
|
|
button.closest('.itemrow').querySelectorAll('input[id^=out]')[0].value = newValue;
|
|
})
|
|
.catch(function (error) {
|
|
showMessageSlider('Unable to change value, please try again.', 'error')
|
|
});
|
|
}
|
|
} // revealItemTiming
|
|
|
|
function removeItemFromRundown(itemrow)
|
|
{
|
|
// Collect data for server processing
|
|
data={};
|
|
data.command = "removeItemFromRundown";
|
|
data.foldername = document.getElementById('foldername').value;
|
|
data.listname = document.getElementById('filebasename').value;
|
|
data.datafile = document.getElementById('datafile').value;
|
|
data.epoch = itemrow.getAttribute('data-spx-epoch'); // an ARRAY INDEX, not itemId!
|
|
|
|
// Remove from DOM
|
|
itemrow.remove();
|
|
event.stopPropagation(); // prevent trying to set focus to a deleted item
|
|
|
|
// Update names for potential save at next sort
|
|
updateFormIndexes()
|
|
|
|
// send server command
|
|
working('Sending ' + data.command + ' request.');
|
|
ajaxpost('',data);
|
|
|
|
} // removeItemFromRundown ended
|
|
|
|
function updateFormIndexes() {
|
|
// This needs to run whenever items are sorted or removed.
|
|
// Sorting routine needs the IndexList for saving sorting to a file.
|
|
// When deleting, this is needed to re-assing form names.
|
|
// 1.0.13 refactored to use epochs and not item indexes.
|
|
// var forms = document.forms;
|
|
let IndexList = []
|
|
let items = document.querySelectorAll('.itemrow');
|
|
items.forEach((item,index) => {
|
|
IndexList.push(item.getAttribute('data-spx-epoch'));
|
|
item.querySelector('form').name = "templates[" + index + "]";
|
|
});
|
|
// console.log('New form list sort order before saving to file', IndexList);
|
|
return IndexList
|
|
} // updateFormIndexes ended
|
|
|
|
function rundownPopup(mode) {
|
|
// get current url
|
|
let url = window.location.href;
|
|
url = url + '/light'
|
|
window.open(url);
|
|
} // rundownPopup ended
|
|
|
|
function SaveNewSortOrder() {
|
|
// This will save the rundown opened in SPX, for instance when
|
|
// items dragged to new sorting order. Maybe other events also?
|
|
working('saving');
|
|
let data={};
|
|
data.newTemplateOrderArr = updateFormIndexes(); // refactored to fix delete + sort bug.
|
|
data.rundownfile = document.getElementById('datafile').value;
|
|
// console.log('SaveNewSortOrder', data);
|
|
ajaxpost('/gc/sortTemplates',data);
|
|
|
|
} // SaveNewSortOrder ended
|
|
|
|
function saveTemplateItemChanges(elementID) {
|
|
// This is a utility function using Axios to POST
|
|
// A FORM to server and we need to update GUI / DOM
|
|
// accordingly.
|
|
|
|
// Note: items might be sorted, so must check actual index
|
|
// of called button.
|
|
let FormNro = getDomIndexByElementId(elementID)
|
|
|
|
working('Saving data from ElementID [' + elementID + '] = domIndex [' + FormNro + '] to server...');
|
|
// console.log('%c Saving data from ElementID [' + elementID + '] = domIndex [' + FormNro + '] to server...', 'background: #F0F; color: #000');
|
|
let urlPath='/gc/saveItemChanges';
|
|
|
|
data={};
|
|
data.DataFields=[];
|
|
const form = document.getElementById('form' + elementID);
|
|
iterator = -1;
|
|
[...form.elements].forEach((input,index) => {
|
|
if (input.getAttribute('data-role')=="userEditable")
|
|
{
|
|
let updatedObj = {}
|
|
updatedObj.field = input.getAttribute('data-update');
|
|
updatedObj.value = input.value;
|
|
data.DataFields.push(updatedObj)
|
|
iterator += 1;
|
|
// console.log('Looking for ' + 'datapreview_' + FormNro + '_' + iterator);
|
|
if (document.getElementById('datapreview_' + elementID + '_' + iterator))
|
|
{
|
|
let temp1 = input.value.replace(/\n/g, ' '); // remove \n globally to support text areas
|
|
let temp2 = temp1.replace(/\r/g, ''); // remove \r globally to support text areas
|
|
document.getElementById('datapreview_' + elementID + '_' + iterator).innerText=temp2;
|
|
}
|
|
}
|
|
});
|
|
|
|
data.rundownfile = document.getElementById('datafile').value;
|
|
data.TemplateIdx = FormNro; // sort order on file and in dom should correspond directly
|
|
axios.post(urlPath,data,
|
|
{
|
|
// headers: { 'Content-Type': 'multipart/form-data' }
|
|
})
|
|
.then(function (response) {
|
|
statusbar(response.data);
|
|
working('');
|
|
ToggleExpand(elementID); // changed from domIndex to ElementID
|
|
})
|
|
.catch(function (error) {
|
|
working('');
|
|
if (error.response) {
|
|
statusbar(error.response.data,'error')
|
|
console.log(error.response.data);
|
|
console.log(error.response.status);
|
|
console.log(error.response.headers);
|
|
} else if (error.request) {
|
|
statusbar('SPX server connection error','error')
|
|
console.log(error.request);
|
|
} else {
|
|
statusbar('SPX request error, see console','warn')
|
|
console.log('Error', error.message);
|
|
}
|
|
console.log(error.config);
|
|
});
|
|
} // saveTemplateItemChanges
|
|
|
|
function saveTemplateItemChangesByElement(itemrow) {
|
|
working('Saving changed data to server...');
|
|
data={};
|
|
data.DataFields=[];
|
|
const form = itemrow.querySelectorAll('form')[0];
|
|
[...form.elements].forEach((input,index) => {
|
|
if (input.getAttribute('data-role')=="userEditable")
|
|
{
|
|
let updatedObj = {}
|
|
updatedObj.field = input.getAttribute('data-update');
|
|
updatedObj.value = input.value;
|
|
if (input.type === 'checkbox') {
|
|
if (input.checked) {
|
|
updatedObj.value = '1';
|
|
} else {
|
|
updatedObj.value = '0';
|
|
}
|
|
}
|
|
data.DataFields.push(updatedObj)
|
|
}
|
|
});
|
|
|
|
|
|
data.rundownfile = document.getElementById('datafile').value;
|
|
data.epoch = itemrow.getAttribute("data-spx-epoch");
|
|
axios.post('/gc/saveItemChanges',data, {})
|
|
.then(function (response) {
|
|
statusbar(response.data[0]);
|
|
|
|
// the 2nd item from response is the html snippet for the GUI (improved in 1.0.7)
|
|
itemrow.querySelector('.truncate').innerHTML = response.data[1];
|
|
|
|
working('');
|
|
ToggleExpand(itemrow);
|
|
})
|
|
.catch(function (error) {
|
|
working('');
|
|
if (error.response) {
|
|
statusbar(error.response.data,'error')
|
|
console.log(error.response.data);
|
|
console.log(error.response.status);
|
|
console.log(error.response.headers);
|
|
} else if (error.request) {
|
|
statusbar('SPX server connection error','error')
|
|
console.log(error.request);
|
|
} else {
|
|
statusbar('SPX request error, see console','warn')
|
|
console.log('Error', error.message);
|
|
}
|
|
console.log(error.config);
|
|
});
|
|
} // saveTemplateItemChanges
|
|
|
|
function showItemIDs(show=true) {
|
|
// Show item ID's in GUI
|
|
let rows = document.querySelectorAll('.itemrow');
|
|
rows.forEach((item,index) => {
|
|
if (show==true) {
|
|
item.querySelector('.utilityOverlay').classList.remove('hidden');
|
|
// document.getElementById('rundownInfoMessage').style='display:flex;';
|
|
} else if (show==false) {
|
|
item.querySelector('.utilityOverlay').classList.add('hidden');
|
|
// document.getElementById('rundownInfoMessage').style='display:none;';
|
|
} else {
|
|
item.querySelector('.utilityOverlay').classList.toggle('hidden');
|
|
}
|
|
});
|
|
} // showItemIDs ended
|
|
|
|
function setMasterButtonStates(itemrow, debugMessage='') {
|
|
// this triggers when row focuses or when Play or Stop pressed or item changed.
|
|
|
|
if (!document.getElementById('MasterTOGGLE')) return; // this page does not have buttons...
|
|
|
|
let TOGGLEBUTTON = document.getElementById('MasterTOGGLE');
|
|
let UPDATEBUTTON = document.getElementById('MasterUPDATE');
|
|
let CONTINBUTTON = document.getElementById('MasterCONTINUE');
|
|
|
|
if (document.querySelectorAll('.itemrow').length<1){
|
|
// no items on page, so disable buttons and bale out
|
|
if (TOGGLEBUTTON) {TOGGLEBUTTON.classList.add('disabled')};
|
|
if (UPDATEBUTTON) {UPDATEBUTTON.classList.add('disabled')};
|
|
if (CONTINBUTTON) {CONTINBUTTON.classList.add('disabled')};
|
|
return
|
|
}
|
|
|
|
// console.clear();
|
|
// console.log('States check: "' + typeof itemrow + '"', itemrow);
|
|
// console.log('CHECKING: setMasterButtonStates of item ' + itemrow.getAttribute('data-spx-epoch') + ' (' + debugMessage + ')');
|
|
|
|
// this gfx only has in-animation
|
|
if ( itemrow.querySelector('input[name="RundownItem[out]"]') && itemrow.querySelector('input[name="RundownItem[steps]"]').value=="none" ) {
|
|
// console.log('This items outmode = none, so not changing button states');
|
|
// maybe add a play and setTimeout to reset quickly...
|
|
return;
|
|
}
|
|
|
|
var stepsVal = itemrow.querySelector('input[name="RundownItem[steps]"]').value || 0;
|
|
var stepsleft= itemrow.getAttribute('data-spx-stepsleft');
|
|
var steps = parseInt(stepsVal);
|
|
var onAirState = String(itemrow.getAttribute('data-spx-onair') || false)
|
|
var changState = String(itemrow.getAttribute('data-spx-changed') || false)
|
|
onAirState = onAirState.toLowerCase();
|
|
changState = changState.toLowerCase();
|
|
var onair = (onAirState == 'true'); // make bool
|
|
var chngd = (changState == 'true'); // make bool
|
|
|
|
// console.log('Onair', onair, 'Changed', chngd, 'Steps', steps );
|
|
|
|
// CONTINUE
|
|
CONTINBUTTON.classList.add('disabled');
|
|
if (stepsleft > 1 && onair ) {
|
|
setTimeout(function () { CONTINBUTTON.classList.remove('disabled'); }, 3);
|
|
}
|
|
|
|
// PLAY - STOP TOGGLE
|
|
if (onair) {
|
|
// console.log('Focused item IS playing (so change it to STOP)');
|
|
TOGGLEBUTTON.classList.remove('disabled')
|
|
TOGGLEBUTTON.innerText = TOGGLEBUTTON.getAttribute('data-spx-stoptext');
|
|
TOGGLEBUTTON.classList.remove('bg_green');
|
|
TOGGLEBUTTON.classList.add('bg_red');
|
|
} else {
|
|
// console.log('Focused item NOT playing (so change it to PLAY)');
|
|
TOGGLEBUTTON.classList.remove('disabled')
|
|
TOGGLEBUTTON.innerText = TOGGLEBUTTON.getAttribute('data-spx-playtext');
|
|
TOGGLEBUTTON.classList.remove('bg_red');
|
|
TOGGLEBUTTON.classList.add('bg_green');
|
|
}
|
|
} // setTGButtonStates ended
|
|
|
|
function hideMessageSlider() {
|
|
if (!document.getElementById('messageSlider')) { return }
|
|
anime({
|
|
targets: '#messageSlider',
|
|
opacity: 0,
|
|
translateY: [0,-200],
|
|
translateX: '-50%',
|
|
duration: 300,
|
|
easing: 'easeInCubic'
|
|
});
|
|
} // hideMessageSlider
|
|
|
|
function showMessageSlider(msg, type='info', persist=false) {
|
|
// show a sliding message at the top of the page
|
|
// type: happy, info, warn, error
|
|
// persist: if true, do not auto-hide
|
|
// Example usage in templates (or extensions, remember to
|
|
// add dom element:)
|
|
/*
|
|
<div id="messageSlider" style="opacity:0;"></div>
|
|
*/
|
|
/*
|
|
top?.showMessageSlider?.('Hello graphics operator!', 'happy');
|
|
*/
|
|
let txt = msg;
|
|
let typ = type; // happy, info, warn, error (classes happyMsg, infoMsg, warnMsg, errorMsg...)
|
|
let domElement
|
|
if (document.getElementById('messageSlider')) {
|
|
domElement = document.getElementById('messageSlider');
|
|
} else {
|
|
console.warn('SPX message: ' + msg);
|
|
return;
|
|
}
|
|
|
|
switch (msg) {
|
|
case 'invalidCSV':
|
|
txt = 'Invalid CSV. See help: import and export'
|
|
typ = 'error'
|
|
}
|
|
|
|
domElement.innerHTML = txt;
|
|
domElement.classList = typ + 'Msg';
|
|
domElement.style.transform = 'translateX(-50%)'; // init pos
|
|
domElement.style.opacity = 0;
|
|
anime({
|
|
targets: '#messageSlider',
|
|
opacity: [0,1],
|
|
translateY: [-200,0],
|
|
translateX: '-50%',
|
|
delay: 100,
|
|
duration: 200,
|
|
easing: 'easeOutCubic'
|
|
});
|
|
|
|
if (persist) {return}; // do not animate out
|
|
|
|
if (messageSliderOutTimer) {
|
|
clearTimeout(messageSliderOutTimer)
|
|
}
|
|
|
|
messageSliderOutTimer = setTimeout(() => {
|
|
anime({
|
|
targets: '#messageSlider',
|
|
opacity: [1,0],
|
|
translateY: [0,-200],
|
|
translateX: '-50%',
|
|
duration: 300,
|
|
easing: 'easeInCubic'
|
|
});
|
|
}, 2000);
|
|
} // showMessageSlider
|
|
|
|
function spx_system(cmd,servername='') {
|
|
// system command handler
|
|
let sysCmd = cmd.toUpperCase()
|
|
let data = {};
|
|
switch (sysCmd) {
|
|
case 'OPENWEBRENDERER':
|
|
window.open("/renderer", 'gcwebrender', 'width=1920,height=1080,scrollbars=yes,location=yes,status=yes');
|
|
break;
|
|
|
|
case 'OPENWEBRENDERER_SCALABLE':
|
|
var w = 1280;
|
|
var h = 720;
|
|
var left = (screen.width/2)-(w/2);
|
|
var top = (screen.height/2)-(h/2);
|
|
var OPT = '_blank,toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left;
|
|
window.open('/renderer/scalable', 'gcwebrender2', OPT);
|
|
break;
|
|
|
|
case 'OPENDATAFOLDER':
|
|
data.command = 'DATAFOLDER';
|
|
break;
|
|
|
|
case 'OPENTEMPLATEFOLDER':
|
|
data.command = 'TEMPLATEFOLDER';
|
|
break;
|
|
|
|
case 'CHECKCONNECTIONS':
|
|
// console.log('Checking connections to servers...', servername);
|
|
data.command = 'CHECKCONNECTIONS';
|
|
data.server = servername;
|
|
break;
|
|
|
|
case 'RESTARTSERVER':
|
|
// this will kill 'nodejs' -server and 'forever' utility will restart it again and again...
|
|
data.command = 'RESTARTSERVER';
|
|
data.reloadPage = true;
|
|
break;
|
|
|
|
default:
|
|
console.log('Unknown spx_system identifer: ' + cmd);
|
|
break;
|
|
}
|
|
|
|
AJAXGET('/CCG/system/' + JSON.stringify(data));
|
|
if (data.reloadPage) {
|
|
document.getElementById('SmartPX_App').style.opacity = '0.4';
|
|
setTimeout('location.reload()', 1000);
|
|
}
|
|
} // end spx_system
|
|
|
|
|
|
function tip(msg) {
|
|
// request .....
|
|
// returns .....
|
|
// Describe the function here
|
|
e = document.getElementById('statusbar') || null;
|
|
if (e) {
|
|
document.getElementById('statusbar').innerText=msg;
|
|
event.stopPropagation();
|
|
// playServerAudio('beep', 'We are in TIP function');
|
|
}
|
|
} // tip mgsed
|
|
|
|
|
|
function e(ID) {
|
|
return document.getElementById(ID);
|
|
} // e
|
|
|
|
function ife(ID) {
|
|
if (document.getElementById(ID)) {
|
|
// console.log('IFE Element found: ' + ID);
|
|
return document.getElementById(ID);
|
|
} else {
|
|
// console.log('IFE Element NOT found: ' + ID);
|
|
return false;
|
|
}
|
|
} // ife
|
|
|
|
function spxInit() {
|
|
// executes on page load:
|
|
// - load values from localStorage
|
|
// - init Sortable
|
|
|
|
// Init sortable and saveData onEnd
|
|
if ( ife('identifier').value=="controller" ) {
|
|
sortable = Sortable.create(itemList, {
|
|
handle: '.handle',
|
|
animation: 150,
|
|
disabled: false,
|
|
// sortable.option("disabled", true); // TAI false
|
|
onEnd: function (evt) {
|
|
SaveNewSortOrder();
|
|
},
|
|
});
|
|
}
|
|
|
|
focusRow(0);
|
|
AppState("DEFAULT");
|
|
spx_system('CHECKCONNECTIONS');
|
|
document.getElementById('itemList').style.opacity=1;
|
|
|
|
} // end spxInit
|
|
|
|
function setProfile(profileName) {
|
|
// FIXME: remove?
|
|
// change profile to profileName and save to localStorage
|
|
if (profileName == '') {
|
|
// retrieve from localStorage
|
|
profileName = localStorage.SPX_CT_ProfileName || '...';
|
|
}
|
|
document.getElementById('profname').innerText = profileName;
|
|
localStorage.SPX_CT_ProfileName = profileName;
|
|
} // setProfile ended
|
|
|
|
function statusbar(sMsg, sLevel="x") {
|
|
/* Usage
|
|
- statusbar('This is a normal message')
|
|
- statusbar('Hot warning', "warn")
|
|
- statusbar('Red alert', "error")
|
|
*/
|
|
|
|
if (!document.getElementById('statusbar')) { return } // not in controller
|
|
let msglevel = sLevel.toUpperCase();
|
|
// console.log('statusbar level: [' + msglevel +'], msg: [' + sMsg + '].');
|
|
document.getElementById('statusbar').classList="statusbar";
|
|
switch (msglevel){
|
|
case "ERROR":
|
|
document.getElementById('statusbar').classList.add("statusBarError");
|
|
break;
|
|
case "WARN":
|
|
document.getElementById('statusbar').classList.add("statusBarWarn");
|
|
break;
|
|
}
|
|
document.getElementById('statusbar').innerText=sMsg;
|
|
} // statusbar
|
|
|
|
function stopAll() {
|
|
// Will send STOP commands to all layers used by current rundown.
|
|
// Timeout here allows some time for server to handle the incoming commands.
|
|
// TODO: Far from elegant but kind of works. A better approach would be to
|
|
// develop a server-side function for this.
|
|
// Function implemented in v1.0.8 from ExtraFunctions -library
|
|
let ITEMS = document.querySelectorAll('.itemrow');
|
|
ITEMS.forEach(function (templateItem, itemNro) {
|
|
setTimeout(function(){
|
|
// continueUpdateStop('stop', templateItem); // slower
|
|
playItem(templateItem, 'stop');
|
|
// setItemButtonStates(templateItem, 'stop') // just the UI
|
|
}, (itemNro * 40)); // 20, 40, 60, 80ms etc...
|
|
});
|
|
} // stopAll
|
|
|
|
function swap2HTMLntities(str){
|
|
// This fixes issue #5 re special characters
|
|
str = str.replace(/\\/g, "\") // html entity for backslash
|
|
str = str.replace(/"/g, """) // html entity for dblquote
|
|
str = encodeURIComponent(str); // handle the rest
|
|
return str;
|
|
} // end swap2HTMLntities
|
|
|
|
|
|
function tip(msg) {
|
|
// request .....
|
|
// returns .....
|
|
// Describe the function here
|
|
e = document.getElementById('statusbar') || null;
|
|
if (e) {
|
|
document.getElementById('statusbar').innerText=msg;
|
|
event.stopPropagation();
|
|
// playServerAudio('beep', 'We are in TIP function');
|
|
}
|
|
} // tip mgsed
|
|
|
|
function toggleAutoplay(slider, targ) {
|
|
let Cfgdata = {};
|
|
Cfgdata.key = 'autoplayLocalRenderer'
|
|
if (slider.checked) {
|
|
document.getElementById(targ).innerText = 'YES';
|
|
Cfgdata.val = true;
|
|
} else {
|
|
document.getElementById(targ).innerText = 'NO';
|
|
Cfgdata.val = false;
|
|
}
|
|
// console.log('Sending config change to server', Cfgdata);
|
|
ajaxpost('/gc/saveConfigChanges',Cfgdata);
|
|
} // toggleAutoplay
|
|
|
|
function toggleLRendererHandler(slider, targ) {
|
|
let Cfgdata = {};
|
|
Cfgdata.key = 'disableLocalRenderer'
|
|
if (slider.checked) {
|
|
document.getElementById(targ).innerText = 'YES';
|
|
document.getElementById('previewIF').src = '/templates/empty.html';
|
|
document.getElementById('LocalRendererDisabledNotification').style.opacity = 1;
|
|
Cfgdata.val = true;
|
|
|
|
} else {
|
|
document.getElementById('previewIF').src = '/renderer';
|
|
document.getElementById(targ).innerText = 'NO';
|
|
document.getElementById('LocalRendererDisabledNotification').style.opacity = 0;
|
|
Cfgdata.val = false;
|
|
}
|
|
// console.log('Sending config change to server', Cfgdata);
|
|
ajaxpost('/gc/saveConfigChanges',Cfgdata);
|
|
} // toggleLRendererHandler
|
|
|
|
function toggleSwitchHandler(slider, targ, type) {
|
|
// When program/preview toggle buttons are clicked.
|
|
// console.log('toggleSwitchHandler', slider, targ);
|
|
let data = {}
|
|
data.type = type;
|
|
let handle = slider.querySelector('.vc-handle');
|
|
if (slider.checked) {
|
|
data.command = 'open';
|
|
document.getElementById(targ).innerText = 'ON';
|
|
} else {
|
|
data.command = 'close';
|
|
document.getElementById(targ).innerText = 'OFF';
|
|
}
|
|
handleRendererPopups(data)
|
|
} // toggleSwitchHandler
|
|
|
|
function toggleRundownSettings() {
|
|
// Toggle the rundown settings panel
|
|
let optionsZone = document.getElementById('rdOptionsCollapsible');
|
|
if (optionsZone.style.maxHeight){
|
|
optionsZone.style.maxHeight = null;
|
|
setTimeout(function(){optionsZone.style.display = 'none';}, 300);
|
|
} else {
|
|
optionsZone.style.display = 'block';
|
|
optionsZone.style.maxHeight = optionsZone.scrollHeight + "px";
|
|
}
|
|
} // toggleRundownSettings
|
|
|
|
function toggleTip(targetField, source, attr) {
|
|
document.getElementById(targetField).innerText = source.getAttribute(attr);
|
|
} // toggleTip
|
|
|
|
function toggleActive(ServerName='') {
|
|
// Will enable / disable commands to the server
|
|
// CCG/disable {server:'OVERLAY, disable:false}
|
|
let e = document.getElementById('server'+ ServerName);
|
|
let targetState = false;
|
|
if (e && e.classList.contains('disabledServer')) {
|
|
// Enable
|
|
e.classList.remove('disabledServer')
|
|
targetState = false
|
|
} else {
|
|
// Disable
|
|
e.classList.add('disabledServer')
|
|
targetState = true
|
|
}
|
|
|
|
let data = {};
|
|
data.server = ServerName;
|
|
data.disabled = targetState;
|
|
working('Changing server connection state to [' + targetState + ']...');
|
|
ajaxpost('/CCG/disable',data);
|
|
} // toggleActive
|
|
|
|
function ToggleExpand(rowelement='') {
|
|
if (!rowelement){
|
|
// rowelement not given, get focused line
|
|
rowelement=getFocusedRow();
|
|
}
|
|
// console.log('Toggle row id ' + rowelement.id);
|
|
let Expanded = rowelement.querySelector('#Expanded')
|
|
let Collapsed = rowelement.querySelector('#Collapsed')
|
|
|
|
if (Collapsed.style.display=="none") {
|
|
// We were open, so lets COLLAPSE
|
|
Collapsed.style.display="block";
|
|
Expanded.style.display="none";
|
|
AppState('DEFAULT');
|
|
} else {
|
|
// We were closed, so lets EXPAND and set focus to first text field
|
|
Collapsed.style.display="none";
|
|
Expanded.style.display="block";
|
|
if (Expanded.querySelectorAll('.gcinput').length>0) {
|
|
// if there are input fields, go to editor mode (and disable dragsort)
|
|
let firstFormElement = Expanded.querySelectorAll('.gcinput')[0];
|
|
if (firstFormElement.type=="text") {
|
|
firstFormElement.focus();
|
|
firstFormElement.select();
|
|
}
|
|
AppState('EDITING');
|
|
}
|
|
}
|
|
} // ToggleExpand
|
|
|
|
function updateItem(itemrow) {
|
|
// request ..... rundown template index
|
|
// returns ..... ajax response message to GUI
|
|
// Will send data object with an index and playlist filename
|
|
// and playout handler will parse required data and so on.
|
|
|
|
if (!itemrow) {itemrow = getFocusedRow() }
|
|
|
|
working('Sending update request.');
|
|
data = {};
|
|
data.command = 'update';
|
|
data.datafile = document.getElementById('datafile').value;
|
|
|
|
data.relpath = itemrow.querySelector('[name="RundownItem[relpath]"]').value;
|
|
data.playserver = itemrow.querySelector('[name="RundownItem[playserver]"]').value;
|
|
data.playchannel = itemrow.querySelector('[name="RundownItem[playchannel]"]').value;
|
|
data.playlayer = itemrow.querySelector('[name="RundownItem[playlayer]"]').value;
|
|
data.webplayout = itemrow.querySelector('[name="RundownItem[webplayout]"]').value;
|
|
data.epoch = itemrow.getAttribute('data-spx-epoch');
|
|
|
|
data.fields = [];
|
|
let CurrentDomFields = itemrow.querySelectorAll("[data-update]");
|
|
|
|
CurrentDomFields.forEach((item,index) => {
|
|
let formField={};
|
|
formField.field = item.getAttribute('data-update');
|
|
formField.value = item.value;
|
|
if (item.type === 'checkbox') {
|
|
if (item.checked) {
|
|
formField.value = '1'
|
|
} else {
|
|
formField.value = '0'
|
|
}
|
|
}
|
|
data.fields.push(formField);
|
|
});
|
|
|
|
// Note, data here IS in internal array format [{"field":"f1", "value":"Hello"},{"field":"f2", "value":"World"},]
|
|
// and MUST NOT change. Format it downstream for different renderers.
|
|
ajaxpost('/gc/playout',data, 'true');
|
|
} // updateItem
|
|
|
|
function playServerAudio(sfx,message=''){
|
|
console.log('%cPlaying sound (' + sfx + ') on server. Log: ' + message + '.', 'background: #622; color: #fff');
|
|
data = {};
|
|
data.sound = sfx;
|
|
data.info = message;
|
|
ajaxpost('/gc/playaudio/', data)
|
|
} // playServerAudio
|
|
|
|
|
|
|
|
function working(StatusMsg){
|
|
if ( !document.getElementById('working') ) return ; // not in controller
|
|
if (StatusMsg==""){
|
|
document.getElementById('working').innerText="";
|
|
document.getElementById('working').style.display="none";
|
|
} else {
|
|
document.getElementById('working').innerText=StatusMsg;
|
|
document.getElementById('working').style.display="inline-block";
|
|
}
|
|
} //working
|
|
|
|
function toggleNormalRenderer(cmd) {
|
|
// Added in 1.1.0
|
|
// This [closing of popup] can happen in any SPX view,
|
|
// not necessarily in the controller view...
|
|
let Cfgdata = {};
|
|
Cfgdata.key = 'renderer'
|
|
if (cmd == 'open') {
|
|
Cfgdata.val = 'popup';
|
|
} else {
|
|
Cfgdata.val = 'normal';
|
|
}
|
|
ajaxpost('/gc/saveConfigChanges',Cfgdata);
|
|
showMessageSlider('Renderer changed to ' + Cfgdata.val + '.')
|
|
|
|
if ( !document.getElementById('previewIF') ) return ; // not in controller
|
|
if (cmd == 'open') {
|
|
// Hide inline renderer, use popup
|
|
document.getElementById('previewBG').style.display='none';
|
|
document.getElementById('previewIF').src='/templates/empty.html';
|
|
} else {
|
|
// Use normal renderer
|
|
document.getElementById('previewBG').style.display='block';
|
|
document.getElementById('previewIF').src='/renderer';
|
|
}
|
|
} // toggleNormalRenderer
|
|
|
|
|
|
// This script is gotten straight from https://www.w3schools.com/howto/howto_js_draggable.asp
|
|
function dragElement(elmnt) {
|
|
if (!elmnt) { return } // element not found, bail out
|
|
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
|
if (document.getElementById(elmnt.id + "HeaderBar")) {
|
|
// if present, the header is where you move the DIV from:
|
|
document.getElementById(elmnt.id + "HeaderBar").onmousedown = dragMouseDown;
|
|
} else {
|
|
// otherwise, move the DIV from anywhere inside the DIV:
|
|
elmnt.onmousedown = dragMouseDown;
|
|
}
|
|
|
|
function dragMouseDown(e) {
|
|
e = e || window.event;
|
|
e.preventDefault();
|
|
// get the mouse cursor position at startup:
|
|
pos3 = e.clientX;
|
|
pos4 = e.clientY;
|
|
document.onmouseup = closeDragElement;
|
|
// call a function whenever the cursor moves:
|
|
document.onmousemove = elementDrag;
|
|
}
|
|
|
|
function elementDrag(e) {
|
|
e = e || window.event;
|
|
e.preventDefault();
|
|
// calculate the new cursor position:
|
|
pos1 = pos3 - e.clientX;
|
|
pos2 = pos4 - e.clientY;
|
|
pos3 = e.clientX;
|
|
pos4 = e.clientY;
|
|
// set the element's new position:
|
|
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
|
|
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
|
|
}
|
|
|
|
function closeDragElement() {
|
|
// stop moving when mouse button is released:
|
|
document.onmouseup = null;
|
|
document.onmousemove = null;
|
|
}
|
|
} |