Source: modalScriptExec.js

"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 modalScriptExec_js */

/**
 * Executes a script and uses a comma separated list of options as arguments
 * @param {string} cmd script to execute
 * @param {string} options options parsed as comma separated list of arguments
 * @returns {void}
 */
//eslint-disable-next-line no-unused-vars
function execScriptFromOptions(cmd, options) {
    const args = options[0] !== ''
        ? options[0].split(',')
        : [];
    execScript({"script": cmd, "arguments": args});
}

/**
 * Parses the script arguments definition and creates the form elements
 * @param {string} name Name of the element
 * @param {string} value Element value definition
 * @param {string} defaultValue Default value for the element
 * @returns {Element} Created element
 */
function parseArgument(name, value, defaultValue) {
    if (defaultValue === undefined) {
        defaultValue = '';
    }
    switch(value) {
        case 'hidden':
        case 'text':
        case 'password':
        case 'checkbox':
            return createScriptDialogEl({"type": value, "name": name, "value": defaultValue});
        // No default
    }
    const match = value.split(';');
    const values = match.slice(1);
    switch(match[0]) {
        case 'select':
        case 'radio':
        case 'list':
            return createScriptDialogEl({"type": match[0], "name": name, "value": values, "defaultValue": defaultValue});
        // No default
    }
    return elCreateEmpty('div', {});
}

/**
 * Creates the form to ask for script arguments
 * @param {Element} container Element to append the form elements
 * @param {Array} scriptArguments Array of script arguments
 * @param {object} values The default values for the form elements
 * @returns {void}
 */
function scriptArgsToForm(container, scriptArguments, values) {
    elClear(container);
    for (let i = 0, j = scriptArguments.length; i < j; i++) {
        const match = scriptArguments[i].split('|');
        const label = match.length === 1
            ? scriptArguments[i]
            : match[0];
        const value = values !== undefined
            ? values[label] !== undefined
                ? values[label]
                : ''
            : '';
        if (match.length === 1) {
            match.push('text');
        }
        const input = parseArgument(match[0], match[1], value);
        if (match[1] === 'hidden') {
            container.appendChild(input);
        }
        else {
            container.appendChild(
                elCreateNodes('div', {"class": ["form-group", "row", "mb-3"]}, [
                    elCreateText('label', {"class": ["col-sm-4", "col-form-label"]}, label),
                    elCreateNode('div', {"class": ["col-sm-8"]}, input)
                ])
            );
        }
    }
}

/**
 * Executes a script and asks for argument values
 * @param {object} cmd script and arguments object to execute
 * @returns {void}
 */
function execScript(cmd) {
    if (cmd.arguments.length === 0) {
        sendAPI("MYMPD_API_SCRIPT_EXECUTE", {
            "script": cmd.script,
            "event": "user",
            "arguments": {}
        }, null, false);
    }
    else {
        scriptArgsToForm(elGetById('modalScriptExecArgumentsList'), cmd.arguments);
        elGetById('modalScriptExecScriptnameInput').value = cmd.script;
        elGetById('modalScriptExecTitle').textContent = tn('Execute script');
        uiElements.modalScriptExec.show();
    }
}

/**
 * Parses the script dialog definition and creates the form elements
 * @param {object} data Definition of form element to create
 * @returns {Element} Created element
 */
function createScriptDialogEl(data) {
    if (data.defaultValue === undefined) {
        data.defaultValue = data.value;
    }
    switch(data.type) {
        case 'text':
        case 'password':
        case 'hidden':
            return elCreateEmpty('input', {"name": data.name, "value": data.value, "type": data.type, "class": ["form-control"]});
        case 'checkbox': {
            const btn = elCreateText('button', {"name": data.name, "type": "button", "data-value": "true",
                "class": ["btn", "btn-sm", "btn-secondary", "mi", "chkBtn"]}, "radio_button_unchecked");
            if (data.value === 'true') {
                btn.classList.add('active');
                btn.textContent = 'check';
            }
            btn.addEventListener('click', function(event) {
                    toggleBtnChk(event.target, undefined);
                }, false);
            return btn;
        }
        case 'select': {
            const sel = elCreateEmpty('select', {"name": data.name, "class": ["form-select"]});
            for (let i = 0; i < data.value.length; i++) {
                const title = data.displayValue && data.displayValue[i]
                    ? data.displayValue[i]
                    : data.value[i];
                sel.appendChild(
                    elCreateText('option', {"value": data.value[i]}, title)
                );
            }
            if (data.defaultValue !== '') {
                sel.value = data.defaultValue;
            }
            return sel;
        }
        case 'radio': {
            const radios = [];
            for (let i = 0; i < data.value.length; i++) {
                const title = data.displayValue && data.displayValue[i]
                    ? data.displayValue[i]
                    : data.value[i];
                radios.push(
                    elCreateNodes('div', {"class": ["form-check"]}, [
                        elCreateEmpty('input', {"name": data.name, "value": data.value[i], "type": "radio", "class": ["form-check-input", "ms-0", "me-3"]}),
                        elCreateText('label', {"class": ["form-check-label"]}, title)
                    ])
                );
                if (data.defaultValue === data.value[i]) {
                    radios[radios.length - 1].querySelector('input').setAttribute('checked', 'checked');
                }
            }
            return elCreateNodes('div', {"data-name": data.name, "data-type": "radios"}, radios);
        }
        case 'list': {
            const rows = [];
            for (let i = 0; i < data.value.length; i++) {
                const title = data.displayValue && data.displayValue[i].title
                    ? data.displayValue[i].title
                    : data.value[i];
                const lines = [];
                let hasDesc = false;
                if (data.displayValue && data.displayValue[i].text) {
                    lines.push(elCreateText('p', {"class": ["mb-1"]}, data.displayValue[i].text));
                    hasDesc = true;
                }
                if (data.displayValue && data.displayValue[i].small) {
                    lines.push(elCreateText('small', {}, data.displayValue[i].small));
                    hasDesc = true;
                }
                rows.push(
                    elCreateNodes('li', {"data-value": data.value[i], "class": ["list-group-item", "clickable"]}, [
                        elCreateNodes('div', {"class": ["d-flex", "justify-content-between", "align-items-start"]}, [
                            elCreateText((hasDesc === true ? 'h5' : 'p'), {"class": ["mb-1"]}, title),
                            pEl.selectBtn.cloneNode(true),
                        ]),
                        ... lines
                    ])
                );
            }
            const lg = elCreateNodes('ul', {"data-name": data.name, "data-type": "list", "class": ["list-group"]}, rows);
            lg.addEventListener('click', function(event) {
                event.preventDefault();
                const target = event.target.nodeName === 'LI'
                    ? event.target
                    : event.target.closest('li');
                const active = target.classList.contains('active');
                if (active === false) {
                    target.classList.add('active');
                    target.querySelector('button').textContent = ligatures.checked;
                }
                else {
                    target.classList.remove('active');
                    target.querySelector('button').textContent = ligatures.unchecked;
                }
            }, false);
            return lg;
        }
        // No default
    }
    return elCreateEmpty('div', {});
}

/**
 * Creates the form for the script dialog
 * @param {Element} container Element to append the form elements
 * @param {object} data Definition of form elements
 * @returns {void}
 */
function scriptDialogToForm(container, data) {
    elClear(container);
    for (let i = 0, j = data.length; i < j; i++) {
        const input = createScriptDialogEl(data[i]);
        if (data[i].type === 'hidden') {
            container.appendChild(input);
        }
        else {
            container.appendChild(
                elCreateNodes('div', {"class": ["form-group", "row", "mb-3"]}, [
                    elCreateText('label', {"class": ["col-sm-4", "col-form-label"]}, data[i].name),
                    elCreateNode('div', {"class": ["col-sm-8"]}, input)
                ])
            );
        }
    }
}

/**
 * Shows the script dialog and asks for input
 * @param {object} params Jsonrpc params from script_dialog method
 * @returns {void}
 */
function showScriptDialog(params) {
    scriptDialogToForm(elGetById('modalScriptExecArgumentsList'), params.data);
    elGetById('modalScriptExecScriptnameInput').value = params.callback;
    elGetById('modalScriptExecTitle').textContent = params.message;
    uiElements.modalScriptExec.show();
}

/**
 * Executes a script after asking for argument values
 * @param {Element} target triggering element
 * @returns {void}
 */
//eslint-disable-next-line no-unused-vars
function execScriptArgs(target) {
    const script = elGetById('modalScriptExecScriptnameInput').value;
    const args = formToScriptArgs(elGetById('modalScriptExecArgumentsList'));
    uiElements.modalScriptExec.hide();
    // The hide function requires some time, wait before executing the script.
    // Workaround for scripts that respond with a dialog.
    setTimeout(function() {
        sendAPI("MYMPD_API_SCRIPT_EXECUTE", {
            "script": script,
            "event": "user",
            "arguments": args
        }, null, true);
    }, 250);
}

/**
 * Returns the form elements names and values for script arguments
 * @param {Element} container Element with the form elements
 * @returns {object} Object with form element names and values
 */
function formToScriptArgs(container) {
    const args = {};
    const inputs = container.querySelectorAll('input');
    for (let i = 0, j = inputs.length; i < j; i++) {
        if (inputs[i].type === 'radio') {
            continue;
        }
        args[inputs[i].name] = inputs[i].value;
    }
    const selects = container.querySelectorAll('select');
    for (let i = 0, j = selects.length; i < j; i++) {
        args[selects[i].name] = getSelectValue(selects[i]);
    }
    const btns = container.querySelectorAll('.chkBtn');
    for (let i = 0, j = btns.length; i < j; i++) {
        args[btns[i].name] = getBtnChkValue(btns[i]) === true ? 'true' : 'false';
    }
    const radios = container.querySelectorAll('[data-type=radios]');
    for (let i = 0, j = radios.length; i < j; i++) {
        args[radios[i].getAttribute('data-name')] = getRadioBoxValue(radios[i]);
    }
    const lists = container.querySelectorAll('[data-type=list]');
    for (let i = 0, j = lists.length; i < j; i++) {
        const items = [];
        for (const el of lists[i].querySelectorAll('.active')) {
            items.push(el.getAttribute('data-value'));
        }
        args[lists[i].getAttribute('data-name')] = items.join(';;');
    }
    return args;
}