"use strict";
// SPDX-License-Identifier: GPL-3.0-or-later
// myMPD (c) 2018-2024 Juergen Mang <mail@jcgames.de>
// https://github.com/jcorporation/mympd
/** @module modalScripts_js */
/**
* Initialization functions for the script elements
* @returns {void}
*/
function initModalScripts() {
elGetById('modalScriptsAddArgumentInput').addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
addScriptArgument();
}
}, false);
elGetById('modalScriptsArgumentsInput').addEventListener('click', function(event) {
if (event.target.nodeName === 'OPTION') {
removeScriptArgument(event);
event.stopPropagation();
}
}, false);
elGetById('modalScriptsList').addEventListener('click', function(event) {
event.stopPropagation();
event.preventDefault();
const target = event.target.closest('TR');
if (event.target.nodeName === 'A') {
const action = getData(event.target, 'action');
const script = getData(target, 'script');
switch(action) {
case 'delete':
deleteScript(event.target, script);
break;
case 'execute':
execScript(getData(target, 'href'));
break;
case 'add2home':
addScriptToHome(script, getData(target, 'href'));
break;
default:
logError('Invalid action: ' + action);
}
return;
}
if (checkTargetClick(target) === true) {
showEditScript(getData(target, 'script'));
}
}, false);
elGetById('modalScriptsAddAPIcallBtn').parentNode.addEventListener('show.bs.dropdown', function() {
const dw = elGetById('modalScriptsContentInput').offsetWidth - elGetById('modalScriptsAddAPIcallBtn').parentNode.offsetLeft;
elGetById('modalScriptsAddAPIcallDropdown').style.width = dw + 'px';
}, false);
elGetById('modalScriptsAddFunctionBtn').parentNode.addEventListener('show.bs.dropdown', function() {
const dw = elGetById('modalScriptsContentInput').offsetWidth - elGetById('modalScriptsAddFunctionBtn').parentNode.offsetLeft;
elGetById('modalScriptsAddFunctionDropdown').style.width = dw + 'px';
}, false);
const modalScriptsAPIcallSelectEl = elGetById('modalScriptsAPIcallSelect');
elClear(modalScriptsAPIcallSelectEl);
modalScriptsAPIcallSelectEl.appendChild(
elCreateTextTn('option', {"value": ""}, 'Select method')
);
for (const m of Object.keys(APImethods).sort()) {
modalScriptsAPIcallSelectEl.appendChild(
elCreateText('option', {"value": m}, m)
);
}
modalScriptsAPIcallSelectEl.addEventListener('change', function(event) {
const value = getSelectValue(event.target);
elGetById('modalScriptsAPIdesc').textContent = value !== '' ? APImethods[value].desc : '';
}, false);
const modalScriptsFunctionSelectEl = elGetById('modalScriptsFunctionSelect');
modalScriptsFunctionSelectEl.addEventListener('change', function(event) {
const value = getSelectValue(event.target);
elGetById('modalScriptsFunctionDesc').textContent = value !== '' ? LUAfunctions[value].desc : '';
}, false);
elGetById('modalScriptsImportList').addEventListener('click', function(event) {
const target = event.target.nodeName === 'li'
? event.target
: event.target.closest('li');
if (event.target.nodeName === 'A') {
return;
}
if (target !== null) {
importScript(target);
}
}, false);
}
/**
* Adds a function to the script content element
* @param {Event} event triggering event
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function addScriptFunction(event) {
event.preventDefault();
event.stopPropagation();
const value = getSelectValueId('modalScriptsFunctionSelect');
if (value === '') {
return;
}
const el = elGetById('modalScriptsContentInput');
const [start, end] = [el.selectionStart, el.selectionEnd];
el.setRangeText(LUAfunctions[value].func, start, end, 'end');
BSN.Dropdown.getInstance(elGetById('modalScriptsAddFunctionBtn')).hide();
setFocus(el);
}
/**
* Adds an API call to the script content element
* @param {Event} event triggering event
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function addScriptAPIcall(event) {
event.preventDefault();
event.stopPropagation();
const method = getSelectValueId('modalScriptsAPIcallSelect');
if (method === '') {
return;
}
const el = elGetById('modalScriptsContentInput');
const [start, end] = [el.selectionStart, el.selectionEnd];
const newText =
'local options = {}\n' +
apiParamsToArgs(APImethods[method].params) +
'local rc, result = mympd.api("' + method + '", options)\n' +
'if rc == 0 then\n' +
'\n' +
'end\n';
el.setRangeText(newText, start, end, 'preserve');
BSN.Dropdown.getInstance(elGetById('modalScriptsAddAPIcallBtn')).hide();
setFocus(el);
}
/**
* Adds the documented api params to the options lua table for the add api call function
* @param {object} p parameters object
* @returns {string} lua code
*/
function apiParamsToArgs(p) {
let args = '';
for (const param in p) {
args += 'options["' + param + '"] = ';
switch(p[param].type) {
case APItypes.string:
args += '"' + p[param].example + '"';
break;
case APItypes.array:
args += '{' + p[param].example.slice(1, -1) + '}';
break;
case APItypes.object: {
args += '{}';
break;
}
default:
args += p[param].example;
}
args += '\n';
}
return args;
}
/**
* Saves a script
* @param {Element} target triggering element
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function saveScript(target) {
cleanupModalId('modalScripts');
btnWaiting(target, true);
const args = [];
const argSel = elGetById('modalScriptsArgumentsInput');
for (let i = 0, j = argSel.options.length; i < j; i++) {
args.push(argSel.options[i].text);
}
sendAPI("MYMPD_API_SCRIPT_SAVE", {
"oldscript": getDataId('modalScriptsEditTab', 'id'),
"script": elGetById('modalScriptsScriptInput').value,
"file": getDataId('modalScriptsEditTab', 'file'),
"version": getDataId('modalScriptsEditTab', 'version'),
"order": Number(elGetById('modalScriptsOrderInput').value),
"content": elGetById('modalScriptsContentInput').value,
"arguments": args
}, saveScriptCheckError, true);
}
/**
* Handler for the MYMPD_API_SCRIPT_SAVE jsonrpc response
* @param {object} obj jsonrpc response
* @returns {void}
*/
function saveScriptCheckError(obj) {
if (modalApply(obj) === true) {
showListScripts();
}
}
/**
* Validates a script
* @param {Element} target triggering element
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function validateScript(target) {
cleanupModalId('modalScripts');
btnWaiting(target, true);
sendAPI("MYMPD_API_SCRIPT_VALIDATE", {
"script": elGetById('modalScriptsScriptInput').value,
"content": elGetById('modalScriptsContentInput').value,
}, validateScriptCheckError, true);
}
/**
* Handler for the MYMPD_API_SCRIPT_VALIDATE jsonrpc response
* @param {object} obj jsonrpc response
* @returns {void}
*/
function validateScriptCheckError(obj) {
if (modalApply(obj) === true) {
showModalInfo('Script syntax is valid');
}
}
/**
* Appends an argument to the list of script arguments
* @returns {void}
*/
function addScriptArgument() {
const el = elGetById('modalScriptsAddArgumentInput');
elGetById('modalScriptsArgumentsInput').appendChild(
elCreateText('option', {}, el.value)
);
el.value = '';
}
/**
* Removes an argument from the list of script arguments
* @param {Event} ev triggering element
* @returns {void}
*/
function removeScriptArgument(ev) {
const el = elGetById('modalScriptsAddArgumentInput');
// @ts-ignore
el.value = ev.target.text;
ev.target.remove();
setFocus(el);
}
/**
* Opens the scripts modal and shows the edit tab
* @param {string} script name to edit
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function showEditScriptModal(script) {
uiElements.modalScripts.show();
showEditScript(script);
}
/**
* Opens the scripts modal and shows the list tab
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function showListScriptModal() {
uiElements.modalScripts.show();
showListScripts();
}
/**
* Shows the edit script tab
* @param {string} script script name
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function showEditScript(script) {
cleanupModalId('modalScripts');
elGetById('modalScripts').firstElementChild.classList.remove('modal-dialog-scrollable');
elGetById('modalScriptsContentInput').removeAttribute('disabled');
elGetById('modalScriptsListTab').classList.remove('active');
elGetById('modalScriptsImportTab').classList.remove('active');
elGetById('modalScriptsEditTab').classList.add('active');
elHideId('modalScriptsListFooter');
elHideId('modalScriptsImportFooter');
elShowId('modalScriptsEditFooter');
if (script !== '') {
sendAPI("MYMPD_API_SCRIPT_GET", {"script": script}, parseEditScript, false);
}
else {
setDataId('modalScriptsEditTab', 'id', '');
setDataId('modalScriptsEditTab', 'file', '');
setDataId('modalScriptsEditTab', 'version', 0);
elGetById('modalScriptsScriptInput').value = '';
elGetById('modalScriptsOrderInput').value = '1';
elGetById('modalScriptsAddArgumentInput').value = '';
elClearId('modalScriptsArgumentsInput');
elGetById('modalScriptsContentInput').value = '';
elDisableId('modalScriptsUpdateBtn');
elHideId('modalScriptsEditDescRow');
}
setFocusId('modalScriptsScriptInput');
}
/**
* Parses the MYMPD_API_SCRIPT_GET jsonrpc response
* @param {object} obj jsonrpc response
* @returns {void}
*/
function parseEditScript(obj) {
setDataId('modalScriptsEditTab', 'id', obj.result.script);
setDataId('modalScriptsEditTab', 'file', obj.result.metadata.file);
setDataId('modalScriptsEditTab', 'version', obj.result.metadata.version);
elGetById('modalScriptsScriptInput').value = obj.result.script;
elGetById('modalScriptsOrderInput').value = obj.result.metadata.order;
elGetById('modalScriptsAddArgumentInput').value = '';
if (obj.result.metadata.file !== '' && obj.result.metadata.version > 0) {
elEnableId('modalScriptsUpdateBtn');
elShowId('modalScriptsEditDescRow');
elGetById('modalScriptsEditLink').setAttribute('href', scriptsUri + dirname(obj.result.metadata.file));
}
else {
elDisableId('modalScriptsUpdateBtn');
elHideId('modalScriptsEditDescRow');
}
const selSA = elGetById('modalScriptsArgumentsInput');
selSA.options.length = 0;
for (let i = 0, j = obj.result.metadata.arguments.length; i < j; i++) {
selSA.appendChild(
elCreateText('option', {}, obj.result.metadata.arguments[i])
);
}
elGetById('modalScriptsContentInput').value = obj.result.content;
}
/**
* Shows the list scripts tab
* @returns {void}
*/
function showListScripts() {
cleanupModalId('modalScripts');
elGetById('modalScripts').firstElementChild.classList.remove('modal-dialog-scrollable');
elGetById('modalScriptsListTab').classList.add('active');
elGetById('modalScriptsEditTab').classList.remove('active');
elGetById('modalScriptsImportTab').classList.remove('active');
elShowId('modalScriptsListFooter');
elHideId('modalScriptsEditFooter');
elHideId('modalScriptsImportFooter');
getScriptList(true);
}
/**
* Deletes a script after confirmation
* @param {EventTarget} el triggering element
* @param {string} script script to delete
* @returns {void}
*/
function deleteScript(el, script) {
cleanupModalId('modalScripts');
showConfirmInline(el.parentNode.previousSibling, tn('Do you really want to delete the script?'), tn('Yes, delete it'), function() {
sendAPI("MYMPD_API_SCRIPT_RM", {
"script": script
}, deleteScriptCheckError, true);
});
}
/**
* Handler for the MYMPD_API_SCRIPT_RM jsonrpc response
* @param {object} obj jsonrpc response
* @returns {void}
*/
function deleteScriptCheckError(obj) {
if (modalListApply(obj) === true) {
getScriptList(true);
}
}
/**
* Gets the list of scripts
* @param {boolean} all true = get all scripts, false = get all scripts with pos > 0
* @returns {void}
*/
function getScriptList(all) {
sendAPI("MYMPD_API_SCRIPT_LIST", {
"all": all
}, parseScriptList, true);
}
/**
* Parses the MYMPD_API_SCRIPT_LIST jsonrpc response
* @param {object} obj jsonrpc response
* @returns {void}
*/
function parseScriptList(obj) {
const table = elGetById('modalScriptsList');
const tbodyScripts = table.querySelector('tbody');
elClear(tbodyScripts);
const mainmenuScripts = elGetById('scripts');
elClear(mainmenuScripts);
const triggerScripts = elGetById('modalTriggerScriptInput');
elClear(triggerScripts);
const widgetScripts = elGetById('modalHomeWidgetScriptInput');
elClear(widgetScripts);
if (checkResult(obj, table, 'table') === false) {
return;
}
const timerActions = elCreateEmpty('optgroup', {"id": "timerActionsScriptsOptGroup", "label": tn('Script')});
setData(timerActions, 'value', 'script');
const scriptListLen = obj.result.data.length;
if (scriptListLen > 0) {
obj.result.data.sort(function(a, b) {
return a.metadata.order - b.metadata.order;
});
for (let i = 0; i < scriptListLen; i++) {
//script list in main menu
if (obj.result.data[i].metadata.order > 0) {
const a = elCreateNodes('a', {"class": ["dropdown-item", "alwaysEnabled", "py-2"], "href": "#"}, [
elCreateText('span', {"class": ["mi", "me-2"]}, "code"),
elCreateText('span', {}, obj.result.data[i].name)
]);
setData(a, 'href', {"script": obj.result.data[i].name, "arguments": obj.result.data[i].metadata.arguments});
mainmenuScripts.appendChild(a);
}
//script list in scripts modal
const tr = elCreateNodes('tr', {"title": tn('Edit')}, [
elCreateText('td', {}, obj.result.data[i].name),
elCreateText('td', {}, obj.result.data[i].metadata.order),
elCreateNodes('td', {"data-col": "Action"}, [
elCreateText('a', {"href": "#", "data-title-phrase": "Delete", "data-action": "delete", "class": ["me-2", "mi", "color-darkgrey"]}, 'delete'),
elCreateText('a', {"href": "#", "data-title-phrase": "Execute", "data-action": "execute", "class": ["me-2", "mi", "color-darkgrey"]}, 'play_arrow'),
elCreateText('a', {"href": "#", "data-title-phrase": "Add to homescreen", "data-action": "add2home", "class": ["me-2", "mi", "color-darkgrey"]}, 'add_to_home_screen')
])
]);
setData(tr, 'script', obj.result.data[i].name);
tr.setAttribute('data-file', obj.result.data[i].metadata.file);
setData(tr, 'href', {"script": obj.result.data[i].name, "arguments": obj.result.data[i].metadata.arguments});
tbodyScripts.appendChild(tr);
//script list select for timers, triggers and home widgets
const option = elCreateText('option', {"value": obj.result.data[i].name}, obj.result.data[i].name);
addOptionToScriptSelect(timerActions, option, obj.result.data[i].metadata.arguments);
addOptionToScriptSelect(triggerScripts, option, obj.result.data[i].metadata.arguments);
addOptionToScriptSelect(widgetScripts, option, obj.result.data[i].metadata.arguments);
}
}
if (scriptListLen === 0) {
elHide(mainmenuScripts.previousElementSibling);
}
else {
elShow(mainmenuScripts.previousElementSibling);
}
//update timer actions select
const old = elGetById('timerActionsScriptsOptGroup');
if (old) {
old.replaceWith(timerActions);
}
else {
elGetById('modalTimerActionInput').appendChild(timerActions);
}
}
/**
* Add's an option to the script select
* @param {HTMLElement} sel Select element to populate
* @param {HTMLElement} opt Option element to add
* @param {object} args Script arguments object
* @returns {void}
*/
function addOptionToScriptSelect(sel, opt, args) {
const optEl = opt.cloneNode(true);
setData(optEl, 'arguments', {"arguments": args});
sel.appendChild(optEl);
}
/**
* Shows the import scripts tab
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function showImportScript() {
cleanupModalId('modalScripts');
elGetById('modalScripts').firstElementChild.classList.add('modal-dialog-scrollable');
elGetById('modalScriptsListTab').classList.remove('active');
elGetById('modalScriptsEditTab').classList.remove('active');
elGetById('modalScriptsImportTab').classList.add('active');
elHideId('modalScriptsListFooter');
elHideId('modalScriptsEditFooter');
elShowId('modalScriptsImportFooter');
const list = elGetById('modalScriptsImportList');
elClear(list);
httpGet(subdir + '/proxy?uri=' + myEncodeURI(scriptsImportUri + 'index.json'), function(obj) {
for (const key in obj) {
const script = obj[key];
const clickable = elGetById('modalScriptsList').querySelector('[data-file="' + key + '"') === null
? 'clickable'
: 'disabled';
list.appendChild(
elCreateNodes('li', {"data-script": key, "class": ["list-group-item", "list-group-item-action", clickable],
"title": tn("Import"), "data-title-phrase": "Import"}, [
elCreateNodes('div', {"class": ["d-flex", "w-100", "justify-content-between"]}, [
elCreateText('h5', {}, script.name),
elCreateText('a', {"href": scriptsUri + dirname(key), "target": "_blank", "class": ["mi", "text-success"],
"data-title": tn("Open"), "data-title-phrase": "Open"}, 'open_in_browser')
]),
elCreateNodes('div', {"class": ["d-flex", "w-100", "justify-content-between"]}, [
elCreateText('p', {"class": ["mb-1"]}, script.desc),
elCreateText('small', {}, 'v' + script.version)
])
])
);
}
}, true);
}
/**
* Shows the edit script tab and imports a script
* @param {EventTarget} target Event target
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function importScript(target) {
const script = target.getAttribute('data-script');
showEditScript('');
elDisableId('modalScriptsContentInput');
httpGet(subdir + '/proxy?uri=' + myEncodeURI(scriptsImportUri + script), function(text) {
doImportScript(text);
}, false);
}
/**
* Imports a script from the mympd-scripts repository
* @param {string} text Script to import
* @returns {boolean} true on success, else false
*/
function doImportScript(text) {
const lines = text.split('\n');
const firstLine = lines.shift();
let obj;
let rc = true;
try {
obj = JSON.parse(firstLine.substring(firstLine.indexOf('{')));
const scriptArgEl = elGetById('modalScriptsArgumentsInput');
scriptArgEl.options.length = 0;
for (let i = 0, j = obj.arguments.length; i < j; i++) {
scriptArgEl.appendChild(
elCreateText('option', {}, obj.arguments[i])
);
}
elGetById('modalScriptsScriptInput').value = obj.name;
elGetById('modalScriptsOrderInput').value = obj.order;
setDataId('modalScriptsEditTab', 'file', obj.file);
setDataId('modalScriptsEditTab', 'version', obj.version);
elGetById('modalScriptsContentInput').value = lines.join('\n');
}
catch(error) {
showModalAlert({
"error": {
"message": "Can not parse script metadata."
}
});
logError('Can not parse script metadata:' + firstLine);
logError(error);
rc = false;
}
elEnableId('modalScriptsContentInput');
setFocusId('modalScriptsContentInput');
return rc;
}
/**
* Updates a script from the mympd-scripts repository
* @returns {void}
*/
//eslint-disable-next-line no-unused-vars
function updateScript() {
cleanupModalId('modalScripts');
btnWaitingId('modalScriptsUpdateBtn', true);
const importFile = getDataId('modalScriptsEditTab', 'file',);
const currentVersion = getDataId('modalScriptsEditTab', 'version');
if (importFile === '' || currentVersion === '') {
return;
}
httpGet(subdir + '/proxy?uri=' + myEncodeURI(scriptsImportUri + 'index.json'), function(obj) {
if (obj[importFile] === undefined) {
showModalAlert({
"error": {
"message": "Can not find script in repository."
}
});
btnWaitingId('modalScriptsUpdateBtn', false);
return;
}
if (obj[importFile].version === currentVersion) {
showModalInfo("Script is up-to-date.");
btnWaitingId('modalScriptsUpdateBtn', false);
return;
}
elDisableId('modalScriptsContentInput');
httpGet(subdir + '/proxy?uri=' + myEncodeURI(scriptsImportUri + importFile), function(text) {
if (doImportScript(text) === true) {
showModalInfo("Script successfully updated.");
}
btnWaitingId('modalScriptsUpdateBtn', false);
}, false);
}, true);
}