Source: contextMenu.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 contextMenu_js */

/**
 * Shows a context menu as popover or offcanvas
 * @param {Event} event triggering event
 * @returns {void}
 */
function showContextMenu(event) {
    event.preventDefault();
    event.stopPropagation();
    //get the correct dom node with data for the triggering event
    let target = event.target.nodeName === 'SPAN'
               ? event.target.parentNode
               : event.target;
    if (target.nodeName === 'SMALL') {
        target = target.parentNode;
    }
    if (target.nodeName === 'TD') {
        target = target.closest('tr');
    }
    else if (target.parentNode.classList.contains('card')) {
        target = target.parentNode;
    }
    else if (target.parentNode.parentNode.classList.contains('card')) {
        target = target.parentNode.parentNode;
    }
    else if (target.parentNode.classList.contains('list-group-item')) {
        target = target.parentNode;
    }
    else if (target.parentNode.parentNode.classList.contains('list-group-item')) {
        target = target.parentNode.parentNode;
    }
    else if (target.parentNode.parentNode.parentNode.classList.contains('list-group-item')) {
        target = target.parentNode.parentNode.parentNode;
    }

    const contextMenuType = target.getAttribute('data-contextmenu');
    logDebug('Create new context menu of type ' + contextMenuType);
    switch (contextMenuType) {
        case 'NavbarPlayback':
        case 'NavbarQueue':
        case 'NavbarBrowse':
            //use popover style context menu
            showPopover(target, contextMenuType);
            break;
        default:
            //all other context menus are displayed in an offcanvas
            showContextMenuOffcanvas(target, contextMenuType);
    }
}

/**
 * Functions to add the menu items
 */

/**
 * Creates the column check list for views
 * @param {EventTarget} target event target
 * @param {HTMLElement} contextMenuTitle title element
 * @param {HTMLElement} contextMenuBody element to append the menu item
 * @returns {void}
 */
function createMenuViewSettings(target, contextMenuTitle, contextMenuBody) {
    if (app.id !== 'Playback' &&
        app.id !== 'BrowseDatabaseAlbumDetail')
    {
        contextMenuBody.appendChild(
            elCreateNodes('div', {'class': ['row']}, [
                elCreateTextTn('label', {'class': ['col-4','col-form-label']}, 'Mode'),
                elCreateNode('div', {'class': ['col-8']},
                    elCreateNodes('div', {'class': ['btn-group', 'w-100'], "id": "viewSettingsMode"}, [
                        elCreateTextTn('button', {"class": ["btn", "btn-secondary"], 'data-value': 'table'}, 'Table'),
                        elCreateTextTn('button', {"class": ["btn", "btn-secondary"], 'data-value': 'grid'}, 'Grid'),
                        elCreateTextTn('button', {"class": ["btn", "btn-secondary"], 'data-value': 'list'}, 'List')
                    ])
                )
            ])
        );
        toggleBtnGroupValueId('viewSettingsMode', settings['view' + app.id].mode);
        for (const btn of elGetById('viewSettingsMode').childNodes) {
            btn.addEventListener('click', function(event) {
                toggleBtnGroup(event.target);
                event.preventDefault();
                event.stopPropagation();
            }, false);
        }
        contextMenuBody.appendChild(
            elCreateEmpty('div', {"class": ["dropdown-divider2", "mb-3"]})
        );
    }
    if (app.id === 'BrowseDatabaseAlbumDetail') {
        createMenuColumnsAppid(target, 'BrowseDatabaseAlbumDetailInfo', contextMenuTitle, contextMenuBody);
        contextMenuBody.appendChild(
            elCreateEmpty('div', {"class": ["dropdown-divider2"]})
        );
        const contextMenuSubtitle = elCreateTextTn('h4', {"class": ["offcanvas-title", "mt-4", "mb-2"]}, 'Song list');
        contextMenuBody.appendChild(contextMenuSubtitle);
    }
    createMenuColumnsAppid(target, app.id, contextMenuTitle, contextMenuBody);
}

/**
 * Creates the column check list for views
 * @param {EventTarget} target event target
 * @param {string} appid application id
 * @param {HTMLElement} contextMenuTitle title element
 * @param {HTMLElement} contextMenuBody element to append the menu item
 * @returns {void}
 */
function createMenuColumnsAppid(target, appid, contextMenuTitle, contextMenuBody) {
    const menu = elCreateEmpty('form', {});
    menu.setAttribute('id', appid + 'FieldsSelect');
    setViewOptions(appid, menu);
    contextMenuBody.classList.add('px-3');
    contextMenuBody.appendChild(menu);
    const applyEl = elCreateTextTn('button', {"class": ["btn", "btn-success", "w-100", "mt-2"]}, 'Apply');
    contextMenuBody.appendChild(applyEl);
    applyEl.addEventListener('click', function(eventClick) {
        eventClick.preventDefault();
        saveView(appid);
    }, false);
}

/**
 * Adds a divider to the context menu
 * @param {HTMLElement} contextMenuBody element to append the divider
 * @returns {void}
 */
function addDivider(contextMenuBody) {
    if (contextMenuBody.lastChild &&
        contextMenuBody.lastChild.nodeName === 'A')
    {
        contextMenuBody.appendChild(
            elCreateEmpty('div', {"class": ["dropdown-divider"]})
        );
    }
}

/**
 * Adds a menu item to the context menu
 * @param {HTMLElement} contextMenuBody element to append the menu item
 * @param {object} cmd the command
 * @param {string} text menu text, will be translated
 * @returns {void}
 */
function addMenuItem(contextMenuBody, cmd, text) {
    const a = elCreateTextTn('a', {"class": ["dropdown-item"], "href": "#"}, text);
    setData(a, 'href', cmd);
    contextMenuBody.appendChild(a);
}

/**
 * Callback function to create the navbar context menu body
 * @param {EventTarget} target triggering element
 * @param {HTMLElement} popoverBody element to append the menu items
 * @returns {void}
 */
function addMenuItemsNavbarActions(target, popoverBody) {
    const type = target.getAttribute('data-contextmenu');
    switch(type) {
        case 'NavbarPlayback':
            addMenuItem(popoverBody, {"cmd": "openModal", "options": ["modalPlayback"]}, 'Playback settings');
            addDivider(popoverBody);
            addMenuItemsSingleActions(popoverBody);
            addMenuItemsConsumeActions(popoverBody);
            addDivider(popoverBody);
            addMenuItem(popoverBody, {"cmd": "appGoto", "options": ["Playback", undefined, undefined]}, 'Show playback');
            break;
        case 'NavbarQueue':
            addMenuItem(popoverBody, {"cmd": "sendAPI", "options": [{"cmd": "MYMPD_API_QUEUE_CLEAR"}]}, 'Clear');
            addMenuItem(popoverBody, {"cmd": "sendAPI", "options": [{"cmd": "MYMPD_API_QUEUE_CROP"}]}, 'Crop');
            addMenuItem(popoverBody, {"cmd": "sendAPI", "options": [{"cmd": "MYMPD_API_QUEUE_SHUFFLE"}]}, 'Shuffle');
            addDivider(popoverBody);
            addMenuItem(popoverBody, {"cmd": "appGoto", "options": ["Queue", "Current", undefined]}, 'Show queue');
            addMenuItem(popoverBody, {"cmd": "appGoto", "options": ["Queue", "LastPlayed", undefined]}, 'Show last played');
            addMenuItem(popoverBody, {"cmd": "gotoJukebox", "options": []}, 'Show jukebox queue');
            break;
        case 'NavbarBrowse':
            addMenuItem(popoverBody, {"cmd": "updateDB", "options": ["", false]}, 'Update database');
            addMenuItem(popoverBody, {"cmd": "updateDB", "options": ["", true]}, 'Rescan database');
            addDivider(popoverBody);
            if (features.featTags === true) {
                addMenuItem(popoverBody, {"cmd": "appGoto", "options": ["Browse", "Database", undefined]}, 'Show browse database');
            }
            addMenuItem(popoverBody, {"cmd": "appGoto", "options": ["Browse", "Filesystem", undefined]}, 'Show browse filesystem');
            addMenuItem(popoverBody, {"cmd": "appGoto", "options": ["Browse", "Playlist", undefined]}, 'Show browse playlists');
            addMenuItem(popoverBody, {"cmd": "appGoto", "options": ["Browse", "Radio", undefined]}, 'Show browse webradio');
            break;
        default:
            logError('Invalid type: ' + type);
    }
}

/**
 * Callback function to create the disc context menu body
 * @param {EventTarget} target triggering element
 * @param {HTMLElement} contextMenuTitle element to set the menu header
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @returns {void}
 */
function addMenuItemsAlbumTagActions(target, contextMenuTitle, contextMenuBody) {
    const dataNode = target.parentNode.parentNode;
    const type = getData(dataNode, 'type');
    const value = getData(dataNode, ucFirst(type));
    const albumId = getData(dataNode, 'AlbumId');
    const albumName = getData(dataNode, 'name');

    addMenuItem(contextMenuBody, {"cmd": "appendQueue", "options": [type, [albumId, value]]}, 'Append to queue');
    addMenuItem(contextMenuBody, {"cmd": "appendPlayQueue", "options": [type, [albumId, value]]}, 'Append to queue and play');
    if (features.featWhence === true &&
        currentState.currentSongId !== -1)
    {
        addMenuItem(contextMenuBody, {"cmd": "insertAfterCurrentQueue", "options": [type, [albumId, value]]}, 'Insert after current playing song');
    }
    addMenuItem(contextMenuBody, {"cmd": "replaceQueue", "options": [type, [albumId, value]]}, 'Replace queue');
    addMenuItem(contextMenuBody, {"cmd": "replacePlayQueue", "options": [type, [albumId, value]]}, 'Replace queue and play');
    if (features.featPlaylists === true) {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "showAddToPlaylist", "options": [type, [albumId, value], [albumName]]}, 'Add to playlist');
    }
}

/**
 * Appends single actions for the queue actions context menu
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @returns {void}
 */
function addMenuItemsSingleActions(contextMenuBody) {
    if (settings.partition.single === '0') {
        if (settings.partition.repeat === true &&
            settings.partition.consume === '0')
        {
            //repeat one song can only work with consume disabled
            addMenuItem(contextMenuBody, {"cmd": "clickSingle", "options": ["oneshot"]}, 'Repeat current song once');
            addMenuItem(contextMenuBody, {"cmd": "clickSingle", "options": ["1"]}, 'Repeat current song');
        }
        else if (settings.partition.repeat === true &&
                 settings.partition.autoPlay === false)
        {
            //single one-shot works only with disabled auto play
            addMenuItem(contextMenuBody, {"cmd": "clickSingle", "options": ["oneshot"]}, 'Stop playback after current song');
        }
    }
    else {
        addMenuItem(contextMenuBody, {"cmd": "clickSingle", "options": ["0"]}, 'Disable single mode');
    }
}

/**
 * Appends consume actions for the queue actions context menu
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @returns {void}
 */
function addMenuItemsConsumeActions(contextMenuBody) {
    if (settings.partition.consume === '0') {
        if (features.featConsumeOneshot === true) {
            addMenuItem(contextMenuBody, {"cmd": "clickConsume", "options": ["oneshot"]}, 'Remove current song after playback');
        }
    }
    else {
        addMenuItem(contextMenuBody, {"cmd": "clickConsume", "options": ["0"]}, 'Disable consume mode');
    }
}

/**
 * Appends album actions to the context menu
 * @param {HTMLElement | EventTarget} dataNode element with the album data
 * @param {HTMLElement} contextMenuTitle element to set the menu header
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @param {string} [albumId] the album id
 * @returns {void}
 */
function addMenuItemsAlbumActions(dataNode, contextMenuTitle, contextMenuBody, albumId) {
    const albumName = [];
    if (dataNode !== null) {
        if (albumId === undefined) {
            albumId = getData(dataNode, 'AlbumId');
        }
        const name = getData(dataNode, 'name');
        if (name !== null) {
            albumName.push(name);
        }
    }
    if (contextMenuTitle !== null) {
        contextMenuTitle.textContent = tn('Album');
    }
    if (app.id !== 'QueueCurrent') {
        addMenuItem(contextMenuBody, {"cmd": "appendQueue", "options": ["album", [albumId]]}, 'Append to queue');
        addMenuItem(contextMenuBody, {"cmd": "appendPlayQueue", "options": ["album", [albumId]]}, 'Append to queue and play');
        if (features.featWhence === true &&
            currentState.currentSongId !== -1)
        {
            addMenuItem(contextMenuBody, {"cmd": "insertAfterCurrentQueue", "options": ["album", [albumId]]}, 'Insert after current playing song');
        }
        addMenuItem(contextMenuBody, {"cmd": "replaceQueue", "options": ["album", [albumId]]}, 'Replace queue');
        addMenuItem(contextMenuBody, {"cmd": "replacePlayQueue", "options": ["album", [albumId]]}, 'Replace queue and play');
    }
    if (features.featPlaylists === true) {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "showAddToPlaylist", "options": ["album", [albumId], albumName]}, 'Add to playlist');
    }
    addDivider(contextMenuBody);
    if (app.id !== 'BrowseDatabaseAlbumDetail') {
        addMenuItem(contextMenuBody, {"cmd": "gotoAlbum", "options": [albumId]}, 'Album details');
    }
    if (features.featStickerAdv === true) {
        addMenuItem(contextMenuBody, {"cmd": "showStickerModal", "options": [albumId, 'mympd_album']}, 'Sticker');
    }
    for (const tag of settings.tagListBrowse) {
        if (dataNode !== null &&
            albumFilters.includes(tag))
        {
            const value = getData(dataNode, tag);
            if (value !== undefined &&
                value.length > 0)
            {
                addMenuItem(contextMenuBody, {"cmd": "gotoAlbumList", "options": [tag, value]}, 'Show all albums from ' + tag);
            }
        }
    }
    if (features.featHome === true &&
        app.id !== 'Home')
    {
        const name = dataNode !== null
            ? getData(dataNode, 'name')
            : '';
        const image = dataNode !== null
            ? getData(dataNode, 'cssImageUrl')
            : '';
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "addAlbumToHome", "options": [albumId, name, image.substr(5,image.length-7)]}, 'Add to homescreen');
    }
}

/**
 * Appends actions for single songs or streams to the context menu
 * @param {HTMLElement} dataNode element with the album data
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @param {string} uri song or stream uri
 * @param {string} type type of the element: song, stream, ...
 * @param {string} name name of the element
 * @returns {void}
 */
function addMenuItemsSongActions(dataNode, contextMenuBody, uri, type, name) {
    if (app.id !== 'QueueCurrent') {
        addMenuItem(contextMenuBody, {"cmd": "appendQueue", "options": [type, [uri]]}, 'Append to queue');
        addMenuItem(contextMenuBody, {"cmd": "appendPlayQueue", "options": [type, [uri]]}, 'Append to queue and play');
        if (features.featWhence === true &&
            currentState.currentSongId !== -1)
        {
            addMenuItem(contextMenuBody, {"cmd": "insertAfterCurrentQueue", "options": [type, [uri], 0, 1, false]}, 'Insert after current playing song');
        }
        addMenuItem(contextMenuBody, {"cmd": "replaceQueue", "options": [type, [uri]]}, 'Replace queue');
        addMenuItem(contextMenuBody, {"cmd": "replacePlayQueue", "options": [type, [uri]]}, 'Replace queue and play');
    }
    if (features.featPlaylists === true) {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "showAddToPlaylist", "options": [type, [uri]]}, 'Add to playlist');
        if (app.id === 'BrowsePlaylistDetail' &&
            getData(dataNode.parentNode.parentNode, 'type') === 'plist')
        {
            const plist = getData(dataNode.parentNode.parentNode, 'uri');
            const pos = getData(dataNode, 'pos');
            addMenuItem(contextMenuBody, {"cmd": "showMoveToPlaylist", "options": [plist, [pos]]}, 'Move to playlist');
        }
    }
    if (type === 'song') {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "songDetails", "options": [uri]}, 'Song details');
        if (features.featStickerAdv === true) {
            addMenuItem(contextMenuBody, {"cmd": "showStickerModal", "options": [uri, type]}, 'Sticker');
        }
    }
    if (features.featHome === true &&
        app.id !== 'Home')
    {
        addDivider(contextMenuBody);
        if (app.id === 'BrowseRadioWebradiodb') {
            const image = getData(dataNode, 'image');
            addMenuItem(contextMenuBody, {"cmd": "addWebRadiodbToHome", "options": [uri, type, name, image]}, 'Add to homescreen');
        }
        else {
            addMenuItem(contextMenuBody, {"cmd": "addSongToHome", "options": [uri, type, name]}, 'Add to homescreen');
        }
    }
    if (app.id === 'BrowseRadioWebradiodb') {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "showWebradiodbDetails", "options": [uri]}, 'Webradio details');
        addMenuItem(contextMenuBody, {"cmd": "saveAsRadioFavorite", "options": [uri]}, 'Add to favorites');
    }
    else if (app.id === 'QueueCurrent' &&
        type === 'webradio')
    {
        addDivider(contextMenuBody);
        const webradioType = getData(dataNode, 'webradioType');
        if (webradioType === 'favorite') {
            addMenuItem(contextMenuBody, {"cmd": "showRadioFavoriteDetails", "options": [uri]}, 'Webradio details');
            addMenuItem(contextMenuBody, {"cmd": "editRadioFavorite", "options": [uri]}, 'Edit webradio favorite');
        }
        else {
            addMenuItem(contextMenuBody, {"cmd": "showWebradiodbDetails", "options": [uri]}, 'Webradio details');
            addMenuItem(contextMenuBody, {"cmd": "saveAsRadioFavorite", "options": [uri]}, 'Add to favorites');
        }
    }
}

/**
 * Appends search actions to the context menu
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @param {string} expression search expression
 * @param {string} sort sort tag
 * @param {boolean} sortdesc descending?
 * @returns {void}
 */
function addMenuItemsSearchActions(contextMenuBody, expression, sort, sortdesc) {
    addMenuItem(contextMenuBody, {"cmd": "appendQueue", "options": ["search", [expression, sort, sortdesc]]}, 'Append to queue');
    addMenuItem(contextMenuBody, {"cmd": "appendPlayQueue", "options": ["search", [expression, sort, sortdesc]]}, 'Append to queue and play');
    if (features.featWhence === true &&
        currentState.currentSongId !== -1)
    {
        addMenuItem(contextMenuBody, {"cmd": "insertAfterCurrentQueue", "options": ["search", [expression, sort, sortdesc], 0, 1, false]}, 'Insert after current playing song');
    }
    addMenuItem(contextMenuBody, {"cmd": "replaceQueue", "options": ["search", [expression, sort, sortdesc]]}, 'Replace queue');
    addMenuItem(contextMenuBody, {"cmd": "replacePlayQueue", "options": ["search", [expression, sort, sortdesc]]}, 'Replace queue and play');
    if (features.featPlaylists === true) {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "showAddToPlaylist", "options": ["search", [expression, sort, sortdesc]]}, 'Add to playlist');
    }
    addDivider(contextMenuBody);
    addMenuItem(contextMenuBody, {"cmd": "appGoto", "options": ["Search", undefined, undefined, 0, undefined, "any", {"tag": "Title", "desc": false}, "", expression]}, 'Show search');
}

/**
 * Appends directory actions to the context menu
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @param {string} baseuri directory
 * @returns {void}
 */
function addMenuItemsDirectoryActions(contextMenuBody, baseuri) {
    //songs must be arranged in one album per folder
    addMenuItem(contextMenuBody, {"cmd": "appendQueue", "options": ["dir", [baseuri]]}, 'Append to queue');
    addMenuItem(contextMenuBody, {"cmd": "appendPlayQueue", "options": ["dir", [baseuri]]}, 'Append to queue and play');
    if (features.featWhence === true &&
        currentState.currentSongId !== -1)
    {
        addMenuItem(contextMenuBody, {"cmd": "insertAfterCurrentQueue", "options": ["dir", [baseuri], 0, 1, false]}, 'Insert after current playing song');
    }
    addMenuItem(contextMenuBody, {"cmd": "replaceQueue", "options": ["dir", [baseuri]]}, 'Replace queue');
    addMenuItem(contextMenuBody, {"cmd": "replacePlayQueue", "options": ["dir", [baseuri]]}, 'Replace queue and play');
    if (features.featPlaylists === true &&
        app.id !== 'Home')
    {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "showAddToPlaylist", "options": ["dir", [baseuri]]}, 'Add to playlist');
    }
    if (app.id === 'BrowseFilesystem') {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "updateDB", "options": [baseuri, false]}, 'Update directory');
        addMenuItem(contextMenuBody, {"cmd": "updateDB", "options": [baseuri, true]}, 'Rescan directory');
    }
    if (baseuri !== app.current.filter) {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "gotoFilesystem", "options": [baseuri, "dir"]}, 'Open directory');
    }
    if (features.featHome === true &&
        app.id !== 'Home')
    {
        addDivider(contextMenuBody);
        addMenuItem(contextMenuBody, {"cmd": "addDirToHome", "options": [baseuri, baseuri]}, 'Add to homescreen');
    }
}

/**
 * Appends actions for webradio favorites to the context menu
 * @param {HTMLElement} target element with the data
 * @param {HTMLElement} contextMenuTitle element for the menu title
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @returns {void}
 */
function addMenuItemsWebradioFavoritesActions(target, contextMenuTitle, contextMenuBody) {
    const type = getData(target, 'type');
    const uri = getData(target, 'uri');
    const name = getData(target, 'name');
    addMenuItemsPlaylistActions(target, contextMenuBody, type, uri, name);
    addDivider(contextMenuBody);
    addMenuItem(contextMenuBody, {"cmd": "showRadioFavoriteDetails", "options": [uri]}, 'Webradio details');
    addMenuItem(contextMenuBody, {"cmd": "editRadioFavorite", "options": [uri]}, 'Edit webradio favorite');
    addMenuItem(contextMenuBody, {"cmd": "deleteRadioFavorites", "options": [[name]]}, 'Delete webradio favorite');
}

/**
 * Appends actions for webradio favorites home icon to the context menu
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @param {string} uri webradio favorite uri
 * @returns {void}
 */
function addMenuItemsWebradioFavoritesHomeActions(contextMenuBody, uri) {
    addDivider(contextMenuBody);
    addMenuItem(contextMenuBody, {"cmd": "editRadioFavorite", "options": [uri]}, 'Edit webradio favorite');
}

/**
 * Appends actions for playlists to the context menu
 * @param {HTMLElement | EventTarget} dataNode element with the data
 * @param {HTMLElement} contextMenuBody element to append the menu items
 * @param {string} type playlist type: plist, smartpls
 * @param {string} uri playlist uri
 * @param {string} name playlist name
 * @returns {void}
 */
function addMenuItemsPlaylistActions(dataNode, contextMenuBody, type, uri, name) {
    addMenuItem(contextMenuBody, {"cmd": "appendQueue", "options": [type, [uri]]}, 'Append to queue');
    addMenuItem(contextMenuBody, {"cmd": "appendPlayQueue", "options": [type, [uri]]}, 'Append to queue and play');
    if (features.featWhence === true &&
        currentState.currentSongId !== -1)
    {
        addMenuItem(contextMenuBody, {"cmd": "insertAfterCurrentQueue", "options": [type, [uri], 0, 1, false]}, 'Insert after current playing song');
    }
    addMenuItem(contextMenuBody, {"cmd": "replaceQueue", "options": [type, [uri]]}, 'Replace queue');
    addMenuItem(contextMenuBody, {"cmd": "replacePlayQueue", "options": [type, [uri]]}, 'Replace queue and play');
    if (features.featHome === true) {
        if (app.id !== 'Home') {
            addDivider(contextMenuBody);
            if (app.id === 'BrowseRadioFavorites') {
                let image = getData(dataNode, 'image');
                if (isHttpUri(image) === false) {
                    image = basename(image, false);
                }
                addMenuItem(contextMenuBody, {"cmd": "addRadioFavoriteToHome", "options": [uri, type, name, image]}, 'Add to homescreen');
            }
            else {
                addMenuItem(contextMenuBody, {"cmd": "addPlistToHome", "options": [uri, type, name]}, 'Add to homescreen');
            }
        }
    }
    if (app.id !== 'BrowsePlaylistList') {
        if (type === 'plist' ||
            type === 'smartpls')
        {
            if (isMPDplaylist(uri) === true) {
                addDivider(contextMenuBody);
                addMenuItem(contextMenuBody, {"cmd": "gotoPlaylist", "options": [uri]}, 'View playlist');
            }
            else {
                addMenuItem(contextMenuBody, {"cmd": "gotoFilesystem", "options": [uri, "plist"]}, 'View playlist');
            }
        }
    }
}

/**
 * Creates the first context menu actions for list context menus
 * @param {EventTarget} target triggering element
 * @param {HTMLElement} contextMenuTitle title element
 * @param {HTMLElement} contextMenuBody content element
 * @returns {boolean} true on success, else false
 */
function createMenuLists(target, contextMenuTitle, contextMenuBody) {
    const dataNode = settings['view' + app.id].mode === 'table'
        ? target.closest('tr')
        : target;
    const type = getData(dataNode, 'type');
    const uri = getData(dataNode, 'uri');
    const name = getData(dataNode, 'name');

    contextMenuTitle.textContent = tn(typeFriendly[type]);
    contextMenuTitle.classList.add('offcanvas-title-' + type);

    switch(app.id) {
        case 'BrowseDatabaseAlbumList':
            addMenuItemsAlbumActions(dataNode, contextMenuTitle, contextMenuBody);
            return true;
        case 'BrowseFilesystem':
        case 'Search':
        case 'BrowseRadioWebradiodb':
        case 'BrowseDatabaseAlbumDetail': {
            switch(type) {
                case 'song':
                case 'stream':
                case 'webradio':
                    addMenuItemsSongActions(dataNode, contextMenuBody, uri, type, name);
                    break;
                case 'dir':
                    addMenuItemsDirectoryActions(contextMenuBody, uri);
                    break;
                case 'plist':
                case 'smartpls':
                    addMenuItemsPlaylistActions(dataNode, contextMenuBody, type, uri, name);
                    break;
                default:
                    return false;
            }
            return true;
        }
        case 'BrowseRadioFavorites':
            addMenuItemsWebradioFavoritesActions(dataNode, contextMenuTitle, contextMenuBody);
            return true;
        case 'BrowsePlaylistList': {
            const smartplsOnly = getData(dataNode, 'smartpls-only');
            if (smartplsOnly === false ||
                type !== 'smartpls')
            {
                addMenuItemsPlaylistActions(dataNode, contextMenuBody, type, uri, name);
                addDivider(contextMenuBody);
                if (type === 'smartpls') {
                    addMenuItem(contextMenuBody, {"cmd": "gotoPlaylist", "options": [uri]}, 'View playlist');
                }
                else {
                    addMenuItem(contextMenuBody, {"cmd": "gotoPlaylist", "options": [uri]}, 'Edit playlist');
                }
                addMenuItem(contextMenuBody, {"cmd": "showRenamePlaylist", "options": [uri]}, 'Rename playlist');
                addMenuItem(contextMenuBody, {"cmd": "showCopyPlaylist", "options": [[uri]]}, 'Copy playlist');
                if (type === 'plist') {
                    addMenuItem(contextMenuBody, {"cmd": "playlistValidateDedup", "options": [uri, true]}, 'Validate and deduplicate playlist');
                }
            }
            addMenuItem(contextMenuBody, {"cmd": "showDelPlaylist", "options": [[uri]]}, 'Delete playlist');
            if (settings.smartpls === true &&
                type === 'smartpls')
            {
                addDivider(contextMenuBody);
                addMenuItem(contextMenuBody, {"cmd": "showSmartPlaylist", "options": [uri]}, 'Edit smart playlist');
                addMenuItem(contextMenuBody, {"cmd": "updateSmartPlaylist", "options": [uri]}, 'Update smart playlist');
            }
            if (features.featStickerAdv === true) {
                addDivider(contextMenuBody);
                addMenuItem(contextMenuBody, {"cmd": "showStickerModal", "options": [uri, 'playlist']}, 'Sticker');
            }
            return true;
        }
        case 'BrowsePlaylistDetail': {
            const table = elGetById('BrowsePlaylistDetailList');
            addMenuItemsSongActions(dataNode, contextMenuBody, uri, type, name);
            if (getData(table, 'ro') === false) {
                addDivider(contextMenuBody);
                const plist = getData(table, 'uri');
                const pos = getData(dataNode, 'pos');
                const playlistLength = getData(table, 'playlistlength');
                addMenuItem(contextMenuBody, {"cmd": "showSetSongPos", "options": [plist, pos]}, 'Move song');
                addMenuItem(contextMenuBody, {"cmd": "removeFromPlaylistPositions", "options": [plist, [pos]]}, 'Remove');
                if (features.featPlaylistRmRange === true) {
                    if (pos > 0) {
                        addMenuItem(contextMenuBody, {"cmd": "removeFromPlaylistRange", "options": [plist, 0, pos]}, 'Remove all upwards');
                    }
                    if (pos + 1 < playlistLength) {
                        addMenuItem(contextMenuBody, {"cmd": "removeFromPlaylistRange", "options": [plist, pos + 1, -1]}, 'Remove all downwards');
                    }
                }
            }
            return true;
        }
        case 'QueueCurrent': {
            const songid = getData(dataNode, 'songid');
            const pos = getData(dataNode, 'pos');
            addMenuItemsSongActions(dataNode, contextMenuBody, uri, type, name);
            addDivider(contextMenuBody);
            if (currentState.currentSongId !== -1 &&
                songid !== currentState.currentSongId &&
                features.featWhence === true)
            {
                addMenuItem(contextMenuBody, {"cmd": "playAfterCurrent", "options": [[songid]]}, 'Play after current playing song');
            }
            if (settings.partition.random === true) {
                addMenuItem(contextMenuBody, {"cmd": "showSetSongPriority", "options": [songid]}, 'Set priority');
            }
            else {
                addMenuItem(contextMenuBody, {"cmd": "showSetSongPos", "options": ["queue", pos]}, 'Move song');
            }
            if (songid === currentState.currentSongId) {
                addMenuItemsSingleActions(contextMenuBody);
                addMenuItemsConsumeActions(contextMenuBody);
            }
            addDivider(contextMenuBody);
            addMenuItem(contextMenuBody, {"cmd": "removeFromQueueIDs", "options": [[songid]]}, 'Remove');
            if (pos > 0) {
                addMenuItem(contextMenuBody, {"cmd": "removeFromQueueRange", "options": [0, pos]}, 'Remove all upwards');
            }
            if (pos + 1 < currentState.queueLength) {
                addMenuItem(contextMenuBody, {"cmd": "removeFromQueueRange", "options": [pos + 1, -1]}, 'Remove all downwards');
            }
            return true;
        }
        case 'QueueLastPlayed': {
            addMenuItemsSongActions(dataNode, contextMenuBody, uri, type, name);
            return true;
        }
        case 'QueueJukeboxSong':
        case 'QueueJukeboxAlbum': {
            const pos = Number(getData(dataNode, 'pos'));
            if (settings.partition.jukeboxMode === 'song') {
                addMenuItemsSongActions(dataNode, contextMenuBody, uri, type, name);
            }
            else if (settings.partition.jukeboxMode === 'album') {
                addMenuItemsAlbumActions(dataNode, null, contextMenuBody);
            }
            addDivider(contextMenuBody);
            addMenuItem(contextMenuBody, {"cmd": "delQueueJukeboxEntries", "options": [[pos]]}, 'Remove');
            return true;
        }
        // No Default
    }
    return false;
}

/**
 * Creates the secondary context menu actions for list context menus
 * @param {EventTarget} target triggering element
 * @param {HTMLElement} contextMenuTitle title element
 * @param {HTMLElement} contextMenuBody content element
 * @returns {boolean} true on success, else false
 */
function createMenuListsSecondary(target, contextMenuTitle, contextMenuBody) {
    switch(app.id) {
        case 'Search':
        case 'QueueCurrent':
        case 'QueueLastPlayed':
        case 'QueueJukeboxSong':
        case 'QueueJukeboxAlbum':
        case 'BrowseFilesystem':
        case 'BrowseDatabaseAlbumDetail':
        case 'BrowsePlaylistDetail': {
            const dataNode = settings['view' + app.id].mode === 'table'
                ? target.closest('tr')
                : target;
            const type = getData(dataNode, 'type');
            const uri = getData(dataNode, 'uri');

            if (isStreamUri(uri) === true ||
                (app.id === 'BrowseFilesystem' && type === 'dir') ||
                (app.id === 'BrowseFilesystem' && type === 'plist') ||
                (app.id === 'BrowseFilesystem' && type === 'smartpls') ||
                (app.id === 'QueueJukeboxAlbum'))
            {
                return false;
            }
            const albumid = getData(dataNode, 'AlbumId');
            if (albumid !== undefined &&
                features.featTags === true)
            {
                contextMenuTitle.textContent = tn('Album');
                contextMenuTitle.classList.add('offcanvas-title-album');
                addMenuItemsAlbumActions(dataNode, null, contextMenuBody);
            }
            else {
                contextMenuTitle.textContent = tn('Directory');
                contextMenuTitle.classList.add('offcanvas-title-dir');
                const baseuri = dirname(uri);
                addMenuItemsDirectoryActions(contextMenuBody, baseuri);
            }
            return true;
        }
        // No Default
    }
    return false;
}

/**
 * Creates the content of the first home icon actions for the context menu
 * @param {EventTarget} target triggering element
 * @param {HTMLElement} contextMenuTitle title element
 * @param {HTMLElement} contextMenuBody content element
 * @returns {boolean} true on success, else false
 */
function createMenuHomeIcon(target, contextMenuTitle, contextMenuBody) {
    const pos = getData(target, 'pos');
    const href = getData(target, 'href');
    if (href === undefined) {
        return false;
    }
    let type = '';
    let actionDesc = '';
    switch(href.cmd) {
        case 'appGoto':
            type = 'view';
            actionDesc = friendlyActions[href.cmd];
            break;
        case 'execScriptFromOptions':
            type = 'script';
            actionDesc = friendlyActions[href.cmd];
            break;
        case 'openExternalLink':
            type = 'externalLink';
            actionDesc = friendlyActions[href.cmd];
            break;
        case 'openModal':
            type = 'modal';
            actionDesc = friendlyActions[href.cmd];
            break;
        default:
            type = href.options[0];
            actionDesc = friendlyActions[href.cmd];
    }
    contextMenuTitle.textContent = tn(typeFriendly[type]);
    contextMenuTitle.classList.add('offcanvas-title-' + type);
    switch(type) {
        case 'plist':
        case 'smartpls':
            addMenuItemsPlaylistActions(target, contextMenuBody, type, href.options[1][0], href.options[1][0]);
            break;
        case 'webradio':
            addMenuItemsPlaylistActions(target, contextMenuBody, type, href.options[1][0], href.options[1][0]);
            addMenuItemsWebradioFavoritesHomeActions(contextMenuBody, href.options[1][0]);
            break;
        case 'dir':
            addMenuItemsDirectoryActions(contextMenuBody, href.options[1][0]);
            break;
        case 'song':
        case 'stream':
            addMenuItemsSongActions(null, contextMenuBody, href.options[1][0], type, href.options[1][0]);
            break;
        case 'search':
            addMenuItemsSearchActions(contextMenuBody, href.options[1][0], href.options[1][1], href.options[1][2]);
            break;
        case 'album':
            addMenuItemsAlbumActions(target, null, contextMenuBody, href.options[1][0]);
            break;
        case 'view':
        case 'externalLink':
        case 'modal':
            addMenuItem(contextMenuBody, {"cmd": "executeHomeIcon", "options": [pos]}, actionDesc);
            break;
        case 'script':
            addMenuItem(contextMenuBody, {"cmd": "executeHomeIcon", "options": [pos]}, actionDesc);
            addMenuItem(contextMenuBody, {"cmd": "showEditScriptModal", "options": [href.options[0]]}, 'Edit script');
            break;
        default:
            logError('Invalid type: ' + type);
    }
    return true;
}

/**
 * Creates the contents of the secondary home icon actions for the context menu
 * @param {EventTarget} target triggering element
 * @param {HTMLElement} contextMenuTitle title element
 * @param {HTMLElement} contextMenuBody content element
 * @returns {boolean} true on success, else false
 */
function createMenuHomeIconSecondary(target, contextMenuTitle, contextMenuBody) {
    const pos = getData(target, 'pos');
    contextMenuTitle.textContent = tn('Home icon');
    contextMenuTitle.classList.add('offcanvas-title-homeicon');
    addMenuItem(contextMenuBody, {"cmd": "editHomeIcon", "options": [pos]}, 'Edit home icon');
    addMenuItem(contextMenuBody, {"cmd": "duplicateHomeIcon", "options": [pos]}, 'Duplicate home icon');
    addMenuItem(contextMenuBody, {"cmd": "deleteHomeIcon", "options": [pos]}, 'Delete home icon');
    return true;
}

/**
 * Creates the contents of the home widget actions for the context menu
 * @param {EventTarget} target triggering element
 * @param {HTMLElement} contextMenuTitle title element
 * @param {HTMLElement} contextMenuBody content element
 * @returns {boolean} true on success, else false
 */
function createMenuHomeWidget(target, contextMenuTitle, contextMenuBody) {
    const pos = getData(target, 'pos');
    contextMenuTitle.textContent = tn('Widget');
    contextMenuTitle.classList.add('offcanvas-title-homewidget');
    addMenuItem(contextMenuBody, {"cmd": "refreshHomeWidget", "options": [pos]}, 'Refresh widget');
    addMenuItem(contextMenuBody, {"cmd": "editHomeWidget", "options": [pos, true]}, 'Edit widget');
    addMenuItem(contextMenuBody, {"cmd": "editHomeWidget", "options": [pos, false]}, 'Duplicate widget');
    addMenuItem(contextMenuBody, {"cmd": "deleteHomeIcon", "options": [pos]}, 'Delete widget');
    return true;
}