Source: settings.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 settings_js */

/**
 * Fetches all myMPD and MPD settings
 * @param {Function} [callback] callback function to execute
 * @returns {void}
 */
function getSettings(callback) {
    settingsParsed = 'no';
    if (callback === undefined ||
        typeof callback !== 'function')
    {
        // only parse the settings
        logWarn('Undefined callback, setting it to parseSettings');
        callback = parseSettings;
    }
    // callback is used to populate modals
    sendAPI('MYMPD_API_SETTINGS_GET', {}, callback, true);
}

/**
 * Parses the MYMPD_API_SETTINGS_GET jsonrpc response
 * @param {object} obj jsonrpc response
 * @returns {boolean} true on success, else false
 */
function parseSettings(obj) {
    if (obj.error) {
        settingsParsed = 'error';
        if (appInited === false) {
            showAppInitAlert(obj.error.message === undefined
                ? tn('Can not parse settings')
                : tn(obj.error.message));
        }
        return false;
    }
    settings = obj.result;

    //check for old cached javascript
    if ('serviceWorker' in navigator && settings.mympdVersion !== myMPDversion) {
        logWarn('Server version (' + settings.mympdVersion + ') not equal client version (' + myMPDversion + '), reloading');
        clearAndReload();
    }

    //set webuiSettings defaults
    for (const key in settingsWebuiFields) {
        if (settings.webuiSettings[key] === undefined &&
            settingsWebuiFields[key].defaultValue !== undefined)
        {
            settings.webuiSettings[key] = settingsWebuiFields[key].defaultValue;
        }
    }

    //set session state
    setSessionState();

    //set features object from settings
    setFeatures();

    //locales
    setLocale(settings.webuiSettings.locale);

    //theme
    let setTheme = settings.webuiSettings.theme;
    if (setTheme === 'auto') {
        setTheme = window.matchMedia &&
            window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
    }
    document.querySelector('html').setAttribute('data-bs-theme', setTheme);

    //compact grids
    if (settings.webuiSettings.compactGrids === true) {
        document.documentElement.style.setProperty('--mympd-card-footer-word-wrap', 'nowrap');
    }
    else {
        document.documentElement.style.setProperty('--mympd-card-footer-word-wrap', 'unset');
    }

    // toggle help
    toggleHelp(settings.webuiSettings.showHelp);

    //background
    if (settings.webuiSettings.theme === 'auto') {
        //in auto mode we set default background
        domCache.body.style.backgroundImage = '';
        document.documentElement.style.setProperty('--mympd-backgroundcolor',
            (setTheme === 'dark' ? '#060708' : '#ffffff')
        );
    }
    else {
        if (settings.webuiSettings.bgImage.indexOf('/assets/') === 0) {
            domCache.body.style.backgroundImage = 'url("' + subdir + myEncodeURI(settings.webuiSettings.bgImage) + '")';
        }
        else if (settings.webuiSettings.bgImage !== '') {
            domCache.body.style.backgroundImage = 'url("' + subdir + '/browse/pics/backgrounds/' + myEncodeURI(settings.webuiSettings.bgImage) + '")';
        }
        else {
            domCache.body.style.backgroundImage = '';
        }
        document.documentElement.style.setProperty('--mympd-backgroundcolor', settings.webuiSettings.bgColor);
    }

    const albumartbg = document.querySelectorAll('body > div.albumartbg');
    for (let i = 0, j = albumartbg.length; i < j; i++) {
        albumartbg[i].style.filter = settings.webuiSettings.bgCssFilter;
    }

    //Navigation and footer
    setNavbarIcons();

    //Mobile view
    setMobileView();
    setUserAgentData();
    setViewport();

    if (settings.webuiSettings.footerSettingsPlayback === true) {
        elShowId('footerSettingsPlayback');
        elHideId('PlaybackSettingsPlayback');
        elHideId('QueueCurrentSettingsPlayback');
    }
    else {
        elHideId('footerSettingsPlayback');
        elShowId('PlaybackSettingsPlayback');
        elShowId('QueueCurrentSettingsPlayback');
    }

    if (settings.webuiSettings.footerPlaybackControls === 'both') {
        elShowId('footerStopBtn');
    }
    else {
        elHideId('footerStopBtn');
    }

    if (settings.webuiSettings.footerSeek === true) {
        elShowId('footerFastRewindBtn');
        elShowId('footerFastForwardBtn');
    }
    else {
        elHideId('footerFastRewindBtn');
        elHideId('footerFastForwardBtn');
    }

    if (settings.webuiSettings.footerPlaybackControlsPopover === true) {
        elShowId('advPlaybackControlsBtn');
    }
    else {
        elHideId('advPlaybackControlsBtn');
    }

    if (settings.partition.jukeboxMode !== 'off') {
        elGetById('footerNextBtn').removeAttribute('disabled');
    }

    //presets
    populatePresetDropdowns();

    //parse mpd settings if connected
    if (settings.partition.mpdConnected === true) {
        parseMPDSettings();
    }
    else {
        logWarn('Skip parsing MPD settings');
    }

    //Info in about modal
    elGetById('modalAboutConnection').textContent = settings.mpdHost.indexOf('/') !== 0 ?
        settings.mpdHost + ':' + settings.mpdPort : settings.mpdHost;

    document.documentElement.style.setProperty('--mympd-grid-size', settings.webuiSettings.gridSize + "px");
    document.documentElement.style.setProperty('--mympd-highlightcolor', settings.partition.highlightColor);
    document.documentElement.style.setProperty('--mympd-highlightcolor-contrast', settings.partition.highlightColorContrast);

    //default limit for all cards
    let limit = settings.webuiSettings.maxElementsPerPage;
    if (limit === 0) {
        limit = maxElementsPerPage;
    }
    app.cards.Home.limit = limit;
    app.cards.Playback.limit = limit;
    app.cards.Queue.tabs.Current.limit = limit;
    app.cards.Queue.tabs.LastPlayed.limit = limit;
    app.cards.Queue.tabs.Jukebox.limit = limit;
    app.cards.Browse.tabs.Filesystem.limit = limit;
    app.cards.Browse.tabs.Playlist.views.List.limit = limit;
    app.cards.Browse.tabs.Playlist.views.Detail.limit = limit;
    app.cards.Browse.tabs.Database.views.TagList.limit = limit;
    app.cards.Browse.tabs.Database.views.AlbumList.limit = limit;
    app.cards.Browse.tabs.Database.views.AlbumDetail.limit = limit;
    app.cards.Search.limit = limit;

    if (app.cards.Browse.tabs.Database.views.AlbumList.sort.tag === '') {
        app.cards.Browse.tabs.Database.views.AlbumList.sort.tag = settings.webuiSettings.browseDatabaseAlbumListSort;
    }

    //scripts
    if (scriptsInited === false) {
        const selectTimerAction = elGetById('modalTimerActionInput');
        elClear(selectTimerAction);
        selectTimerAction.appendChild(
            elCreateNodes('optgroup', {"data-value": "player", "data-label-phrase": "Playback", "label": tn('Playback')}, [
                elCreateTextTn('option', {"value": "startplay"}, 'Start playback'),
                elCreateTextTn('option', {"value": "stopplay"}, 'Stop playback')
            ])
        );

        if (features.featScripting === true) {
            getScriptList(true);
        }
        else {
            elClearId('scripts');
        }
        scriptsInited = true;
    }

    const modalScriptsFunctionSelectEl = elGetById('modalScriptsFunctionSelect');
    elClear(modalScriptsFunctionSelectEl);
    modalScriptsFunctionSelectEl.appendChild(
        elCreateTextTn('option', {"value": ""}, 'Select function')
    );
    for (const m in LUAfunctions) {
        if (LUAfunctions[m].feat === '' ||
            features[LUAfunctions[m].feat] === true)
        {
            modalScriptsFunctionSelectEl.appendChild(
                elCreateText('option', {"value": m}, m)
            );
        }
    }

    //volumebar
    elGetById('volumeBar').setAttribute('min', settings.volumeMin);
    elGetById('volumeBar').setAttribute('max', settings.volumeMax);

    //update actions for table rows
    if (settings.webuiSettings.quickPlayButton === true &&
        settings.webuiSettings.quickRemoveButton === true)
    {
        pEl.actionIcons = pEl.actionMenuPlay;
        pEl.actionDiscIcons = pEl.actionMenuDiscPlay;
        pEl.actionWorkIcons = pEl.actionMenuWorkPlay;
        pEl.actionQueueIcons = pEl.actionMenuRemove;
        pEl.actionJukeboxIcons = pEl.actionMenuPlayRemove;
        pEl.actionPlaylistDetailIcons = pEl.actionMenuPlayRemove;
        pEl.actionPlaylistIcons = pEl.actionMenuPlayRemove;
    }
    else if (settings.webuiSettings.quickPlayButton === true) {
        pEl.actionIcons = pEl.actionMenuPlay;
        pEl.actionDiscIcons = pEl.actionMenuDiscPlay;
        pEl.actionWorkIcons = pEl.actionMenuWorkPlay;
        pEl.actionQueueIcons = pEl.actionMenu;
        pEl.actionJukeboxIcons = pEl.actionMenuPlay;
        pEl.actionPlaylistDetailIcons = pEl.actionMenuPlay;
        pEl.actionPlaylistIcons = pEl.actionMenuPlay;
    }
    else if (settings.webuiSettings.quickRemoveButton === true) {
        pEl.actionIcons = pEl.actionMenu;
        pEl.actionDiscIcons = pEl.actionMenuDisc;
        pEl.actionWorkIcons = pEl.actionMenuWork;
        pEl.actionQueueIcons = pEl.actionMenuRemove;
        pEl.actionJukeboxIcons = pEl.actionMenuRemove;
        pEl.actionPlaylistDetailIcons = pEl.actionMenuRemove;
        pEl.actionPlaylistIcons = pEl.actionMenuRemove;
    }
    else {
        pEl.actionIcons = pEl.actionMenu;
        pEl.actionDiscIcons = pEl.actionMenuDisc;
        pEl.actionWorkIcons = pEl.actionMenuWork;
        pEl.actionQueueIcons = pEl.actionMenu;
        pEl.actionJukeboxIcons = pEl.actionMenu;
        pEl.actionPlaylistDetailIcons = pEl.actionMenu;
        pEl.actionPlaylistIcons = pEl.actionMenu;
    }

    //goto view
    if (app.id === 'QueueJukeboxSong' ||
        app.id === 'QueueJukeboxAlbum')
    {
        gotoJukebox();
    }
    else {
        appRoute();
    }

    //mediaSession support
    if (features.featMediaSession === true) {
        try {
            navigator.mediaSession.setActionHandler('play', clickPlay);
            navigator.mediaSession.setActionHandler('pause', clickPlay);
            navigator.mediaSession.setActionHandler('stop', clickStop);
            navigator.mediaSession.setActionHandler('seekbackward', seekRelativeBackward);
            navigator.mediaSession.setActionHandler('seekforward', seekRelativeForward);
            navigator.mediaSession.setActionHandler('previoustrack', clickPrev);
            navigator.mediaSession.setActionHandler('nexttrack', clickNext);
        }
        catch(err) {
            logWarn('mediaSession.setActionHandler not supported by browser: ' + err.message);
        }
        if (!navigator.mediaSession.setPositionState) {
            logDebug('mediaSession.setPositionState not supported by browser');
        }
    }
    else {
        logDebug('mediaSession not supported by browser or not enabled');
    }

    //finished parse setting, set ui state
    toggleUI();
    applyFeatures();
    setFeedbacktypeId('PlaybackSongFeedback', 'song');
    setFeedbacktypeId('BrowsePlaylistDetailFeedback', 'playlist');
    setFeedbacktypeId('BrowseDatabaseAlbumDetailFeedback', 'mympd_album');
    settingsParsed = 'parsed';
    myMPDready = true;
    return true;
}

/**
 * Parses the MPD options
 * @returns {void}
 */
function parseMPDSettings() {
    elGetById('partitionName').textContent = localSettings.partition;

    if (settings.webuiSettings.bgCover === true) {
        setBackgroundImage(domCache.body, currentSongObj.uri);
    }
    else {
        clearBackgroundImage(domCache.body);
    }

    populateTriggerEvents();

    settings.tagList.sort();
    settings.tagListSearch.sort();
    settings.tagListBrowse.sort();

    filterFields('Playback');

    for (const table of ['Search', 'QueueCurrent', 'QueueLastPlayed',
            'QueueJukeboxSong', 'QueueJukeboxAlbum',
            'BrowsePlaylistDetail', 'BrowseFilesystem', 'BrowseDatabaseTagList',
            'BrowseDatabaseAlbumList', 'BrowseDatabaseAlbumDetail'])
    {
        setView(table);
        filterFields(table);
        if (settings['view' + table].mode === 'table') {
            setCols(table);
        }
        //add all browse tags (advanced action in popover menu)
        const view = 'view' + table + 'Fetch';
        settings[view] = {};
        settings[view].mode = settings['view' + table].mode;
        settings[view].fields = settings['view' + table].fields.slice();
        for (const tag of settings.tagListBrowse) {
            if (settings[view].fields.includes(tag) === false) {
                settings[view].fields.push(tag);
            }
        }
    }
    for (const table of ['Home', 'BrowseRadioFavorites', 'BrowseRadioWebradiodb', 'BrowsePlaylistList']) {
        setView(table);
        if (settings['view' + table].mode === 'table') {
            setCols(table);
        }
    }

    //enforce name for current queue view
    if (settings.viewQueueCurrent.fields.includes('Name') === false &&
        settings.tagList.includes('Name') === true)
    {
        settings.viewQueueCurrentFetch.fields.push('Name');
    }

    //enforce album and albumartist for album list view
    settings['viewBrowseDatabaseAlbumListFetch'] = {};
    settings['viewBrowseDatabaseAlbumListFetch'].mode = settings['viewBrowseDatabaseAlbumList'].mode;
    settings['viewBrowseDatabaseAlbumListFetch'].fields = settings['viewBrowseDatabaseAlbumList'].fields.slice();
    if (settings.viewBrowseDatabaseAlbumListFetch.fields.includes('Album') === false) {
        settings.viewBrowseDatabaseAlbumListFetch.fields.push('Album');
    }
    if (settings.viewBrowseDatabaseAlbumListFetch.fields.includes(tagAlbumArtist) === false) {
        settings.viewBrowseDatabaseAlbumListFetch.fields.push(tagAlbumArtist);
    }

    //enforce disc for album detail list view
    if (settings.viewBrowseDatabaseAlbumDetailFetch.fields.includes('Disc') === false &&
        settings.tagList.includes('Disc'))
    {
        settings.viewBrowseDatabaseAlbumDetailFetch.fields.push('Disc');
    }

    if (settings.viewBrowseDatabaseAlbumDetailFetch.fields.includes('Work') === false &&
        settings.tagList.includes('Work'))
    {
        settings.viewBrowseDatabaseAlbumDetailFetch.fields.push('Work');
    }

    if (features.featTags === false) {
        app.cards.Search.sort.tag = 'filename';
        app.cards.Search.filter = 'filename';
        app.cards.Queue.tabs.Current.filter = 'filename';
        settings.viewQueueCurrent.fields = ["Pos", "Title", "Duration"];
        settings.viewQueueLastPlayed.fields = ["Pos", "Title", "LastPlayed"];
        settings.viewQueueJukeboxSong.fields = ["Pos", "Title"];
        settings.viewQueueJukeboxAlbum.fields = ["Pos", "Title"];
        settings.viewSearch.fields = ["Title", "Duration"];
        settings.viewBrowseFilesystem.fields = ["Type", "Title", "Duration"];
        settings.viewPlayback.fields = [];
    }
    else {
        //construct playback view
        const pbtl = elGetById('PlaybackListTags');
        elClear(pbtl);
        for (let i = 0, j = settings.viewPlayback.fields.length; i < j; i++) {
            let colWidth;
            switch(settings.viewPlayback.fields[i]) {
                case 'Lyrics':
                    colWidth = "col-xl-12";
                    break;
                default:
                    colWidth = "col-xl-6";
            }
            const div = elCreateNodes('div', {"id": "current" + settings.viewPlayback.fields[i],"class": [colWidth]}, [
                elCreateTextTn('small', {}, settings.viewPlayback.fields[i]),
                elCreateEmpty('p', {})
            ]);
            setData(div, 'tag', settings.viewPlayback.fields[i]);
            pbtl.appendChild(div);
        }
        //musicbrainz
        pbtl.appendChild(
            elCreateEmpty('div', {"id": "currentMusicbrainz", "class": ["col-xl-6"]})
        );
        //fill blank card with currentSongObj
        if (currentSongObj !== null) {
            setPlaybackCardTags(currentSongObj);
        }
    }

    if (features.featTags === false) {
        app.cards.Browse.active = 'Filesystem';
    }

    if (settings.tagList.includes('Title')) {
        app.cards.Search.sort.tag = 'Title';
    }

    //fallback from AlbumArtist to Artist
    if (settings.tagList.includes('AlbumArtist')) {
        tagAlbumArtist = 'AlbumArtist';
    }
    else if (settings.tagList.includes('Artist')) {
        tagAlbumArtist = 'Artist';
        if (app.cards.Browse.tabs.Database.views.AlbumList.filter === 'AlbumArtist') {
            app.cards.Browse.tabs.Database.views.AlbumList.filter = tagAlbumArtist;
        }
        if (app.cards.Browse.tabs.Database.views.AlbumList.sort.tag === 'AlbumArtist') {
            app.cards.Browse.tabs.Database.views.AlbumList.sort.tag = tagAlbumArtist;
        }
    }

    addTagList('BrowseDatabaseAlbumListTagDropdown', 'tagListBrowse');
    addTagList('BrowseDatabaseTagListTagDropdown', 'tagListBrowse');
    addTagList('BrowsePlaylistListNavDropdown', 'tagListBrowse');
    addTagList('BrowseFilesystemNavDropdown', 'tagListBrowse');
    addTagList('BrowseRadioFavoritesNavDropdown', 'tagListBrowse');
    addTagList('BrowseRadioWebradiodbNavDropdown', 'tagListBrowse');

    addTagList('QueueCurrentSearchTags', 'tagListSearch');
    addTagList('QueueCurrentSortTagsList', 'tagList');
    addTagList('QueueLastPlayedSearchTags', 'tagListSearch');
    addTagList('QueueJukeboxSongSearchTags', 'tagListSearch');
    addTagList('QueueJukeboxAlbumSearchTags', 'tagListSearch');
    addTagList('BrowsePlaylistDetailSearchTags', 'tagListSearch');
    addTagList('BrowseDatabaseAlbumListSearchTags', 'tagListAlbum');
    addTagList('BrowseDatabaseAlbumListSortTagsList', 'tagListAlbum');
    addTagList('BrowsePlaylistDetailSortTagsDropdown', 'tagList');
    addTagList('BrowseRadioFavoritesSearchTags', '');
    addTagList('BrowseRadioFavoritesSortTagsList', '');
    addTagList('BrowseRadioWebradiodbSearchTags', '');
    addTagList('BrowseRadioWebradiodbSortTagsList', '');
    addTagList('SearchSearchTags', 'tagListSearch');
    addTagList('SearchSortTagsList', 'tagList');

    addTagListSelect('modalSmartPlaylistEditSortInput', 'tagList');
}

/**
 * Toggles the display of the help texts
 * @param {boolean} [mode] true=show help, false=hide help, undefined=toggle
 * @returns {void}
 */
function toggleHelp(mode) {
    if (mode === undefined) {
        mode = document.documentElement.style.getPropertyValue('--mympd-show-help') === 'block'
            ? false
            : true;
    }
    if (mode === true) {
        document.documentElement.style.setProperty('--mympd-show-help', 'block');
        document.documentElement.style.setProperty('--mympd-show-help-btn', 'none');
    }
    else {
        document.documentElement.style.setProperty('--mympd-show-help', 'none');
        document.documentElement.style.setProperty('--mympd-show-help-btn', 'unset');
    }
}

/**
 * Populates the navbar with the icons
 * @returns {void}
 */
function setNavbarIcons() {
    const oldBadgeQueueItems = elGetById('badgeQueueItems');
    let oldQueueLength = 0;
    if (oldBadgeQueueItems) {
        oldQueueLength = Number(oldBadgeQueueItems.textContent);
    }

    const container = elGetById('navbar-main');
    elClear(container);

    if (settings.webuiSettings.showBackButton === true) {
        container.appendChild(
            elCreateNode('div', {"class": ["nav-item", "flex-fill", "text-center"]},
                elCreateNode('a', {"data-title-phrase": "History back", "title": tn("History back"), "href": "#", "class": ["nav-link"]},
                    elCreateText('span', {"class": ["mi"]}, "arrow_back_ios")
                )
            )
        );
        setData(container.firstElementChild.firstElementChild, 'href', {"cmd": "historyBack", "options": []});
    }

    for (const icon of settings.navbarIcons) {
        const id = "nav" + icon.options.join('');
        const btn = elCreateEmpty('div', {"id": id, "class": ["nav-item", "flex-fill", "text-center"]});
        if (id === 'nav' + app.current.card) {
            btn.classList.add('active');
        }
        if (features.featHome === false &&
            icon.options[0] === 'Home')
        {
            elHide(btn);
        }
        const a = elCreateNode('a', {"data-title-phrase": icon.title, "title": tn(icon.title), "href": "#", "class": ["nav-link"]},
            elCreateText('span', {"class": ["mi"]}, icon.ligature)
        );
        if (icon.options.length === 1 &&
            (icon.options[0] === 'Browse' ||
             icon.options[0] === 'Queue' ||
             icon.options[0] === 'Playback'))
        {
            a.setAttribute('data-contextmenu', 'Navbar' + icon.options.join(''));
        }
        if (icon.options[0] === 'Queue' &&
            icon.options.length === 1)
        {
            a.appendChild(
                elCreateText('span', {"id": "badgeQueueItems", "class": ["badge", "bg-secondary"]}, oldQueueLength.toString())
            );
        }
        btn.appendChild(a);
        container.appendChild(btn);
        setData(a, 'href', {"cmd": "appGoto", "options": icon.options});
    }

    //cache elements, reused in appPrepare
    domCache.navbarBtns = container.querySelectorAll('div');
    domCache.navbarBtnsLen = domCache.navbarBtns.length;
}

/**
 * Removes all settings from localStorage
 * @returns {void}
 */
function resetLocalSettings() {
    for (const key in localSettings) {
        localStorage.removeItem(key);
    }
}