Source: init.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 init_js */

/**
 * Initializes / starts the myMPD app
 */

/**
 * Shows an error message on the splashscreen
 * @param {string} text message to display (untranslated)
 * @returns {void}
 */
 function showAppInitAlert(text) {
    const spa = elGetById('splashScreenAlert');
    elClear(spa);
    spa.appendChild(
        elCreateTextTn('p', {"class": ["text-light"]}, text)
    );
    const reloadBtn = elCreateTextTn('button', {"class": ["btn", "btn-light", "me-2", "alwaysEnabled"]}, 'Reload');
    reloadBtn.addEventListener('click', function() {
        clearAndReload();
    }, false);
    const resetBtn = elCreateTextTn('button', {"class": ["btn", "btn-light", "alwaysEnabled"]}, 'Reset');
    resetBtn.addEventListener('click', function() {
        resetLocalSettings();
        clearAndReload();
    }, false);
    spa.appendChild(
        elCreateNodes('p', {}, [
            reloadBtn,
            resetBtn
        ])
    );
}

/**
 * Clears the service worker caches
 * @returns {void}
 */
function clearCache() {
    if ('serviceWorker' in navigator) {
        caches.keys().then(function(cacheNames) {
            cacheNames.forEach(function(cacheName) {
                caches.delete(cacheName);
            });
        });
    }
}

/**
 * Clears the service worker caches and reloads the app
 * @returns {void}
 */
function clearAndReload() {
    clearCache();
    location.reload();
}

/**
 * Starts the app
 * @returns {void}
 */
function appInitStart() {
    getAssets();
    //add app routing event handler
    window.addEventListener('hashchange', function() {
        if (app.goto === false) {
            appRoute();
        }
        else {
            app.goto = false;
        }
    }, false);

    // create pre-generated elements
    createPreGeneratedElements();

    //webapp manifest shortcuts
    const params = new URLSearchParams(window.location.search);
    const action = params.get('action');
    switch(action) {
        case 'clickPlay':
            clickPlay();
            break;
        case 'clickStop':
            clickStop();
            break;
        case 'clickNext':
            clickNext();
            break;
        // No Default
    }

    //update table height on window resize
    window.addEventListener('resize', function() {
        if (resizeTimer !== null) {
            clearTimeout(resizeTimer);
        }
        resizeTimer = setTimeout(function() {
            const list = elGetById(app.id + 'List');
            if (list) {
                setScrollViewHeight(list);
            }
            resizeTimer = null;
        }, 100);
    }, false);

    setMobileView();

    i18nHtml(elGetById('splashScreenAlert'));

    //set loglevel
    if (debugMode === true) {
        settings.loglevel = 4;
    }

    //serviceworker handling
    if ('serviceWorker' in navigator) {
        //add serviceworker
        if (debugMode === false &&
            window.location.protocol === 'https:')
        {
            window.addEventListener('load', function() {
                navigator.serviceWorker.register('sw.js', {scope: subdir + '/'}).then(function(registration) {
                    //Registration was successful
                    logDebug('ServiceWorker registration successful.');
                    registration.update();
                }, function(err) {
                    //Registration failed
                    logError('ServiceWorker registration failed: ' + err);
                });
            });
        }
        //debugMode - delete serviceworker
        if (debugMode === true) {
            let serviceWorkerExists = false;
            navigator.serviceWorker.getRegistrations().then(function(registrations) {
                for (const registration of registrations) {
                    registration.unregister();
                    serviceWorkerExists = true;
                }
                if (serviceWorkerExists === true) {
                    clearAndReload();
                }
                else {
                    clearCache();
                }
            }).catch(function(err) {
                logError('Service Worker unregistration failed: ' + err);
            });
        }
    }

    //show splash screen
    elShowId('splashScreen');
    domCache.body.classList.add('overflow-hidden');
    elGetById('splashScreenAlert').textContent = tn('Fetch myMPD settings');

    //initialize app
    appInited = false;
    settingsParsed = 'no';
    sendAPI("MYMPD_API_SETTINGS_GET", {}, function(obj) {
        parseSettings(obj);
        if (settingsParsed === 'parsed') {
            //connect to websocket in background
            setTimeout(function() {
                webSocketConnect();
            }, 0);
            //app initialized
            elGetById('splashScreenAlert').textContent = tn('Applying settings');
            elGetById('splashScreen').classList.add('hide-fade');
            setTimeout(function() {
                elHideId('splashScreen');
                elGetById('splashScreen').classList.remove('hide-fade');
                domCache.body.classList.remove('overflow-hidden');
            }, 500);
            appInit();
            appInited = true;
            appRoute();
            logDebug('Startup duration: ' + (Date.now() - startTime) + 'ms');
        }
    }, true);
}

/**
 * Initializes the html elements
 * @returns {void}
 */
function appInit() {
    //init links
    initLinks(document);
    //hide popover
    domCache.body.addEventListener('click', function() {
        hidePopover();
    }, false);
    //init modules
    initBrowse();
    initContextMenuOffcanvas();
    initLocalPlayback();
    initModalAbout();
    initModalEnterPin();
    initModalHomeIcon();
    initModalHomeWidget();
    initModalMaintenance();
    initModalMounts();
    initModalNotifications();
    initModalPartitionOutputs();
    initModalPartitions();
    initModalPlaylistAddTo();
    initModalQueueAddTo();
    initModalQueueSave();
    initModalRadioFavoriteEdit();
    initModalSettings();
    initModalSettingsConnection();
    initModalSettingsPlayback();
    initModalScripts();
    initModalSticker();
    initModalVariables();
    initModalSongDetails();
    initModalTimer();
    initModalTrigger();
    initNavs();
    initOutputs();
    initViewBrowseDatabase();
    initViewBrowseFilesystem();
    initViewBrowseRadioFavorites();
    initViewBrowseRadioWebradiodb();
    initViewPlayback();
    initViewPlaylist();
    initPresets();
    initSelectActions();
    initViewQueueCurrent();
    initViewQueueJukebox('QueueJukeboxSong');
    initViewQueueJukebox('QueueJukeboxAlbum');
    initViewQueueLastPlayed();
    initViewSearch();
    //init custom elements
    initElements(domCache.body);
    //add bootstrap native updated event listeners for dropdowns
    const dropdowns = document.querySelectorAll('[data-bs-toggle="dropdown"]');
    for (const dropdown of dropdowns) {
        const positionClass = dropdown.parentNode.classList.contains('dropup')
            ? 'dropup'
            : 'dropdown';
        if (positionClass === 'dropdown') {
            dropdown.parentNode.addEventListener('updated.bs.dropdown', function(event) {
                const menu = event.target.querySelector('.dropdown-menu');
                // reset styles
                menu.style.removeProperty('overflow-y');
                menu.style.removeProperty('overflow-x');
                menu.style.removeProperty('max-height');
                // prevent vertical overflow
                const menuHeight = menu.offsetHeight;
                const offsetY = getYpos(menu);
                const scrollY = getScrollPosY(dropdown);
                const bottomPos = window.innerHeight + scrollY - menuHeight - offsetY;
                if (bottomPos < 0) {
                    menu.style.overflowY = 'auto';
                    menu.style.overflowX = 'hidden';
                    const maxHeight = menuHeight + bottomPos - 10;
                    menu.style.maxHeight = `${maxHeight}px`;
                }
                // prevent horizontal overflow
                const offsetX = getXpos(menu);
                if (offsetX < 0) {
                    menu.style.left = 0;
                }
            }, false);
        }
    }
    //update state on window focus - browser pauses javascript
    window.addEventListener('focus', function() {
        onShow();
    }, false);
    window.addEventListener('pageshow', function() {
        onShow();
    }, false);
    //global keymap
    document.addEventListener('keydown', function(event) {
        if (event.target.tagName === 'INPUT' ||
            event.target.tagName === 'SELECT' ||
            event.target.tagName === 'TEXTAREA' ||
            event.ctrlKey ||
            event.altKey ||
            event.metaKey)
        {
            return;
        }
        const cmd = keymap[event.key];
        if (cmd &&
            typeof window[cmd.cmd] === 'function')
        {
            if (keymap[event.key].feature === undefined ||
                features[keymap[event.key].feature] === true)
            {
                parseCmd(event, cmd);
            }
            event.stopPropagation();
        }
    }, false);

    //websocket
    window.addEventListener('beforeunload', function() {
        webSocketClose();
    });
}

/**
 * Checks the connection state and reconnects the websocket on demand
 * @returns {void}
 */
function onShow() {
    logDebug('Browser focused, update player state');
    getState();
    if (app.id === 'QueueCurrent') {
        execSearchExpression(elGetById('QueueCurrentSearchStr').value);
    }
    websocketKeepAlive();
}

/**
 * Initializes the navigation html elements
 * @returns {void}
 */
function initNavs() {
    domCache.progress.addEventListener('click', function(event) {
        if (currentState.currentSongId >= 0 &&
            currentState.totalTime > 0)
        {
            const seekToPos = Math.ceil((currentState.totalTime * event.clientX) / domCache.progress.offsetWidth);
            clickSeek(seekToPos, false);
        }
    }, false);

    domCache.progress.addEventListener('mousemove', function(event) {
        if ((currentState.state === 'pause' || currentState.state === 'play') &&
            currentState.totalTime > 0)
        {
            domCache.progressPos.textContent = fmtSongDuration(Math.ceil((currentState.totalTime / event.target.offsetWidth) * event.clientX));
            domCache.progressPos.style.display = 'block';
            const w = domCache.progressPos.offsetWidth / 2;
            const posX = event.clientX < w ? event.clientX : (event.clientX < window.innerWidth - w ? event.clientX - w : event.clientX - (w * 2));
            domCache.progressPos.style.left = posX + 'px';
        }
    }, false);

    domCache.progress.addEventListener('mouseout', function() {
        domCache.progressPos.style.display = 'none';
    }, false);

    const navbarMain = elGetById('navbar-main');
    navbarMain.addEventListener('click', function(event) {
        event.preventDefault();
        if (event.target.nodeName === 'DIV') {
            return;
        }
        const target = event.target.nodeName === 'A' ? event.target : event.target.parentNode;
        const href = getData(target, 'href');
        if (href === undefined) {
            return;
        }
        parseCmd(event, href);
    }, false);

    navbarMain.addEventListener('contextmenu', function(event) {
        if (event.target.getAttribute('data-contextmenu') === null &&
            event.target.parentNode.getAttribute('data-contextmenu') === null)
        {
            return;
        }
        showContextMenu(event);
    }, false);
    navbarMain.addEventListener('long-press', function(event) {
        if (event.target.getAttribute('data-contextmenu') === null &&
            event.target.parentNode.getAttribute('data-contextmenu') === null)
        {
            return;
        }
        showContextMenu(event);
    }, false);

    elGetById('scripts').addEventListener('click', function(event) {
        event.preventDefault();
        const target = event.target.nodeName === 'SPAN' ? event.target.parentNode : event.target;
        if (target.nodeName === 'A') {
            target.firstElementChild.textContent = 'start';
            setTimeout(function() {
                target.firstElementChild.textContent = 'code';
            }, 400);
            execScript(getData(target, 'href'));
        }
    }, false);

    domCache.footer.addEventListener('contextmenu', function(event) {
        toggleAdvPlaycontrolsPopover(event);
    }, false);
    domCache.footer.addEventListener('long-press', function(event) {
        toggleAdvPlaycontrolsPopover(event);
    }, false);
}

/**
 * Gets the initial assets
 * @returns {void}
 */
function getAssets() {
    httpGet(subdir + '/assets/i18n/en-US.json', function(obj) {
        phrasesDefault = obj;
    }, true);
    httpGet(subdir + '/assets/ligatures.json', function(obj) {
        materialIcons = obj;
    }, true);
}

/**
 * Handle javascript errors
 * @param {string} msg error message
 * @param {string} url url of the error
 * @param {number} line line of the error
 * @param {number} col column of the error
 * @returns {boolean} false
 */
window.onerror = function(msg, url, line, col) {
    if (settings.loglevel >= 4) {
        showNotification(tn('JavaScript error') + ': ' + msg + ' (' + url + ': ' + line + ':' + col + ')', 'general', 'error');
    }
    //show error also in the console
    return false;
};

/**
 * Configure trusted types to allow service worker registration
 */
if (window.trustedTypes &&
    window.trustedTypes.createPolicy)
{
    window.trustedTypes.createPolicy('default', {
        createScriptURL(dirty) {
            if (dirty === 'sw.js') {
                return 'sw.js';
            }
            throw new Error('Script not allowed: ' + dirty);
       },
       createHTML: string => string
    });
}

//Start app
appInitStart();