Source: views.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 fields_js */

/**
 * Insert the view container
 * @param {string} viewName table name
 * @returns {void}
 */
function setView(viewName) {
    const mode = settings['view' + viewName].mode;
    const curContainer = elGetById(viewName + 'Container');
    const curMode = getData(curContainer, 'viewMode');
    if (curMode === mode) {
        return;
    }
    const newContainer = mode === 'table'
        ? pEl.viewTable.cloneNode(true)
        : mode === 'grid' 
            ? pEl.viewGrid.cloneNode(true)
            : pEl.viewList.cloneNode(true);
    if (viewName === 'Home') {
        newContainer.firstElementChild.classList.remove('row');
    }
    if (curContainer.parentNode.classList.contains('scrollContainer')) {
        // do not insert a scrolling container in an already scrolling parent
        newContainer.classList.remove('scrollContainer', 'table-responsive');
    }
    newContainer.setAttribute('id', viewName + 'Container');
    newContainer.firstElementChild.setAttribute('id', viewName + 'List');
    curContainer.replaceWith(newContainer);
    setData(newContainer, 'viewMode', mode);
    newContainer.firstElementChild.addEventListener('click', function(event) {
        viewClickHandler(event);
    }, false);
    newContainer.firstElementChild.addEventListener('contextmenu', function(event) {
        viewRightClickHandler(event);
    }, false);
    newContainer.firstElementChild.addEventListener('long-press', function(event) {
        viewRightClickHandler(event);
    }, false);
    
    //init drag and drop
    switch(viewName) {
        case 'Home':
        case 'QueueCurrent':
        case 'BrowsePlaylistDetail':
            if (mode === 'table') {
                dragAndDropTable(viewName + 'List');
            }
            else if (mode === 'grid') {
                dragAndDropGrid(viewName + 'List');
            }
            else {
                dragAndDropList(viewName + 'List');
            }
            break;
        // No default
    }
}

/**
 * Central click handler for views
 * @param {MouseEvent} event click event
 * @returns {void}
 */
function viewClickHandler(event) {
    if (event.target.classList.contains('row') ||
        event.target.nodeName === 'CAPTION' ||
        event.target.nodeName === 'TH')
    {
        return;
    }
    //select mode
    if (selectEntry(event) === true) {
        return;
    }
    let target = null;
    const mode = settings['view' + app.id].mode;
    if (mode === 'table') {
        // Links
        if (event.target.nodeName === 'A') {
            if (event.target.parentNode.getAttribute('data-col') === 'Action') {
                handleViewActionClick(event);
            }
            else {
                // allow default link action
            }
            return;
        }
        //table body
        target = event.target.closest('TR');
        if (target === null) {
            return;
        }
        if (target.parentNode.nodeName !== 'TBODY' ||
            checkTargetClick(target) === false)
        {
            return;
        }
    }
    else if (mode === 'grid') {
        if (event.target.nodeName === 'A') {
            if (event.target.getAttribute('href') !== '#') {
                // allow default link action
                return;
            }
            handleViewActionClick(event);
            return;
        }
        // set target to card
        target = event.target.closest('.card');
    }
    else {
        //list view
        if (event.target.nodeName === 'A') {
            if (event.target.getAttribute('href') !== '#') {
                // allow default link action
                return;
            }
            handleViewActionClick(event);
            return;
        }
        // set target to list-group-item
        target = event.target.closest('.list-group-item');
    }
    if (event.target.classList.contains('progress')) {
        if (event.target.getAttribute('disabled') === 'disabled') {
            return;
        }
        clickQuickResumeSong(target);
        return;
    }
    event.preventDefault();
    event.stopPropagation();
    switch(app.id) {
        case 'BrowseDatabaseTagList':
            viewBrowseDatabaseTagListListClickHandler(event, target);
            break;
        case 'BrowseDatabaseAlbumList':
            viewBrowseDatabaseAlbumListListClickHandler(event, target);
            break;
        case 'BrowseDatabaseAlbumDetail':
            viewBrowseDatabaseAlbumDetailListClickHandler(event, target);
            break;
        case 'BrowseFilesystem':
            viewBrowseFilesystemListClickHandler(event, target);
            break;
        case 'BrowsePlaylistList':
            viewPlaylistListListClickHandler(event, target);
            break;
        case 'BrowsePlaylistDetail':
            viewPlaylistDetailListClickHandler(event, target);
            break;
        case 'BrowseRadioFavorites':
            viewBrowseRadioFavoritesListClickHandler(event, target);
            break;
        case 'BrowseRadioWebradiodb':
            viewBrowseRadioWebradiodbListClickHandler(event, target);
            break;
        case 'Home':
            viewHomeClickHandler(event, target);
            break;
        case 'QueueCurrent':
            viewQueueCurrentListClickHandler(event, target);
            break;
        case 'QueueJukeboxAlbum':
        case 'QueueJukeboxSong':
            viewQueueJukeboxListClickHandler(event, target);
            break;
        case 'QueueLastPlayed':
            viewQueueLastPlayedListClickHandler(event, target);
            break;
        case 'Search':
            viewSearchListClickHandler(event, target);
            break;
        // No default
    }
}

/**
 * Central right click handler for views
 * @param {MouseEvent} event click event
 * @returns {void}
 */
function viewRightClickHandler(event) {
    const mode = settings['view' + app.id].mode;
    if (mode === 'table') {
        if (event.target.parentNode.classList.contains('not-clickable') ||
            event.target.parentNode.parentNode.classList.contains('not-clickable') ||
            event.target.nodeName === 'TH')
        {
            return;
        }
        showContextMenu(event);
    }
    else if (mode === 'grid') {
        if (event.target.closest('.card').classList.contains('no-contextmenu')) {
            return;
        }
        if (event.target.classList.contains('card-title') ||
            event.target.classList.contains('card-body') ||
            event.target.parentNode.classList.contains('card-body') ||
            event.target.classList.contains('card-footer'))
        {
            showContextMenu(event);
        }
    }
    else {
        if (event.target.closest('.list-group-item').classList.contains('no-contextmenu')) {
            return;
        }
        showContextMenu(event);
    }
}

/**
 * Handles the click on the actions column
 * @param {MouseEvent} event click event
 * @returns {void}
 */
function handleViewActionClick(event) {
    event.preventDefault();
    const action = event.target.getAttribute('data-action');
    switch(action) {
        case 'popover':
            showContextMenu(event);
            break;
        case 'quickPlay':
            clickQuickPlay(event.target);
            break;
        case 'quickRemove':
            clickQuickRemove(event.target);
            break;
        case 'showSongsByTag': {
            elGetById('SearchSearchStr').value = '';
            const tag = getData(event.target.parentNode.parentNode, 'tag');
            const value = getData(event.target.parentNode.parentNode, 'name');
            gotoSearch(tag, value);
            break;
        }
        case 'showAlbumsByTag':
            elGetById('BrowseDatabaseTagSearchStr').value = '';
            // clear album search input
            elGetById('BrowseDatabaseAlbumListSearchStr').value = '';
            gotoBrowse(event);
            break;
        case 'showStickersByTag': {
            const tag = getData(event.target.parentNode.parentNode, 'tag');
            const value = getData(event.target.parentNode.parentNode, 'name');
            showStickerModal(value, tag);
            break;
        }
        case 'refreshWidget':
            updateHomeWidget(event.target.closest('.card'));
            break;
        default:
            logError('Invalid action: ' + action);
    }
}

/**
 * Return an array of pre-generated action links
 * @param {*} userData custom user data
 * @returns {Array} array of dom nodes
 */
function getActionLinks(userData) {
    switch(app.id) {
        case 'BrowsePlaylistDetail':
            return pEl.actionPlaylistDetailIcons;
        case 'QueueCurrent':
            return pEl.actionQueueIcons;
        case 'QueueJukeboxSong':
        case 'QueueJukeboxAlbum':
            return pEl.actionJukeboxIcons;
        case 'BrowseDatabaseTagList':
            if (settings.tagListAlbum.includes(userData)) {
                return features.featStickerAdv === true
                    ? pEl.actionMenuBrowseDatabaseTagSongsAlbumsStickers
                    : pEl.actionMenuBrowseDatabaseTagSongsAlbums;
            }
            return features.featStickerAdv === true
                ? pEl.actionMenuBrowseDatabaseTagSongsStickers
                : pEl.actionMenuBrowseDatabaseTagSongs;
        case 'BrowseDatabaseAlbumDetail':
            if (userData === 'disc') {
                return pEl.actionDiscIcons;
            }
            else if (userData === 'work') {
                return pEl.actionWorkIcons;
            }
            else {
                return pEl.actionIcons;
            }
        default:
            return pEl.actionIcons;
    }
}

/**
 * Appends action links as child nodes by app.id
 * @param {Element} container container to append the action links as child nodes
 * @param {*} userData custom user data
 * @returns {void}
 */
function addActionLinks(container, userData) {
    const links = getActionLinks(userData);
    for (const link of links) {
        container.appendChild(link.cloneNode(true));
    }
}

/**
 * Saves the fields for views
 * @param {string} viewName table name
 * @returns {void}
 */
//eslint-disable-next-line no-unused-vars
function saveView(viewName) {
    const modeEl = elGetById('viewSettingsMode');
    const mode = modeEl === null
        ? settings["view" + viewName].mode
        : getBtnGroupValueId('viewSettingsMode');
    const params = {
        "view": "view" + viewName,
        "mode": mode,
        "fields": []
    };
    const fieldsForm = elGetById(viewName + 'FieldsSelect');
    const fields = fieldsForm.querySelector('ul').querySelectorAll('li');
    for (let i = 0, j = fields.length; i < j; i++) {
        params.fields.push(fields[i].getAttribute('data-field'));
    }
    sendAPI("MYMPD_API_VIEW_SAVE", params, function() {
        // refresh the settings
        getSettings(parseSettings);
    }, true);
}

/**
 * Filters the selected fields by available fields
 * @param {string} tableName the table name
 * @returns {void}
 */
function filterFields(tableName) {
    //set available tags
    const fields = setFields(tableName);
    //column name
    const set = "view" + tableName;
    settings[set].fields = settings[set].fields.filter(function(value) {
        return fields.includes(value);
    });
    logDebug('Columns for ' + set + ': ' + settings[set]);
}

/**
 * Creates the list element for fields
 * @param {string} field field name
 * @returns {HTMLElement} li element
 */
function createFieldItem(field) {
    return elCreateNodes('li', {"class": ["list-group-item", "clickable"], "draggable": "true", "data-field": field}, [
        document.createTextNode(tn(field)),
        elCreateNodes('div', {'class': ['btn-group', 'float-end', 'fieldsEnabledBtns']}, [
            elCreateText('button', {"class": ["btn", "btn-sm", "mi", "mi-sm", "pt-0", "pb-0"], 'data-action':'remove', 'title': tn('Remove')}, 'close'),
            elCreateText('button', {"class": ["btn", "btn-sm", "mi", "mi-sm", "pt-0", "pb-0"], 'data-action':'up', 'title': tn('Move up')}, 'arrow_upward'),
            elCreateText('button', {"class": ["btn", "btn-sm", "mi", "mi-sm", "pt-0", "pb-0"], 'data-action':'down', 'title': tn('Move down')}, 'arrow_downward'),
        ]),
        elCreateNodes('div', {'class': ['btn-group', 'float-end', 'fieldsAvailableBtns']}, [
            elCreateText('button', {"class": ["btn", "btn-sm", "mi", "mi-sm", "pt-0", "pb-0"], 'data-action':'add', 'title': tn('Add')}, 'add')
        ])
    ]);
}

/**
 * Handles click events for fields
 * @param {Event} event click event
 * @returns {void}
 */
function fieldClick(event) {
    event.stopPropagation();
    event.preventDefault();
    const target = event.target;
    const ul = target.closest('ul');
    const li = target.closest('li');
    if (target.nodeName === 'LI') {
        if (ul.classList.contains('fieldsEnabled')) {
            ul.parentNode.querySelectorAll('ul')[1].appendChild(li);
        }
        else {
            ul.parentNode.querySelector('ul').appendChild(li);
        }
    }
    else if (target.nodeName === 'BUTTON') {
        const action = target.getAttribute('data-action');
        switch(action) {
            case 'remove':
                ul.parentNode.querySelectorAll('ul')[1].appendChild(li);
                break;
            case 'add':
                ul.parentNode.querySelector('ul').appendChild(li);
                break;
            case 'up':
                ul.insertBefore(li, li.previousSibling);
                break;
            case 'down':
                li.nextSibling.after(li);
                break;
            //No Default
        }
    }
}

/**
 * Creates the view settings offcanvas body
 * @param {string} tableName table name
 * @param {HTMLElement} menu element to populate
 * @returns {void}
 */
function setViewOptions(tableName, menu) {
    menu.appendChild(
        elCreateTextTn('h6', {"class": ["dropdown-header"]}, 'Selected')
    );
    const enabledList = elCreateEmpty('ul', {"class": ["list-group", "fieldsEnabled"]});
    for (const field of settings['view' + tableName].fields) {
        enabledList.appendChild(
            createFieldItem(field)
        );
    }
    menu.appendChild(enabledList);
    enabledList.addEventListener('click', function(event) {
        fieldClick(event);
    }, false);
    dragAndDropFieldList(enabledList);
    menu.appendChild(
        elCreateTextTn('h6', {"class": ["dropdown-header","mt-2"]}, 'Available')
    );
    const allFields = setFields(tableName);
    const availableList = elCreateEmpty('ul', {"class": ["list-group", "fieldsAvailable"]});
    for (const field of allFields) {
        if (settings['view' + tableName].fields.includes(field) === true) {
            continue;
        }
        availableList.appendChild(
            createFieldItem(field)
        );
    }
    menu.appendChild(availableList);
    availableList.addEventListener('click', function(event) {
        fieldClick(event);
    }, false);
    dragFieldList(availableList);
}

/**
 * Initializes a list-group for drag of list-items
 * @param {object} list list to enable drag and drop
 * @returns {void}
 */
function dragFieldList(list) {
    list.addEventListener('dragstart', function(event) {
        const target = event.target.nodeName === 'LI'
            ? event.target
            : event.target.closest('li');
        if (target.nodeName === 'LI') {
            target.classList.add('opacity05');
            // @ts-ignore
            event.dataTransfer.setDragImage(target, 0, 0);
            event.dataTransfer.effectAllowed = 'move';
            dragEl = target;
        }
    }, false);
}

/**
 * Initializes a list-group for drag and drop of list-items
 * @param {object} list list to enable drag and drop
 * @returns {void}
 */
function dragAndDropFieldList(list) {
    dragFieldList(list);

    list.addEventListener('dragenter', function(event) {
        if (event.target.closest('form') !== dragEl.closest('form')) {
            return;
        }
        const target = event.target.nodeName === 'LI'
            ? event.target
            : event.target.closest('li');
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.add('dragover');
        }
    }, false);

    list.addEventListener('dragleave', function(event) {
        if (event.target.closest('form') !== dragEl.closest('form')) {
            return;
        }
        const target = event.target.nodeName === 'LI'
            ? event.target
            : event.target.closest('li');
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.remove('dragover');
        }
    }, false);

    list.addEventListener('dragover', function(event) {
        if (event.target.closest('form') !== dragEl.closest('form')) {
            event.dataTransfer.dropEffect = 'none';
            return;
        }
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
    }, false);

    list.addEventListener('drop', function(event) {
        event.stopPropagation();
        event.preventDefault();
        if (event.target.closest('form') !== dragEl.closest('form')) {
            return;
        }
        if (dragEl === undefined ||
            dragEl.nodeName !== 'LI')
        {
            return;
        }
        const target = event.target.nodeName === 'LI'
            ? event.target
            : event.target.closest('li');
        target.classList.remove('dragover');
        const newField = getData(target, 'field');
        const oldField = getData(dragEl, 'field');
        if (oldField === newField) {
            return;
        }
        target.parentNode.insertBefore(dragEl, target);
    }, false);

    list.addEventListener('dragend', function() {
        dragEl.classList.remove('opacity05');
        dragEl = undefined;
    }, false);
}

/**
 * Sets the available fields
 * @param {string} tableName table name
 * @returns {object} array of available columns
 */
function setFields(tableName) {
    switch(tableName) {
        case 'BrowsePlaylistList': {
            const tags = ["Type", "Name", "Last-Modified", "Thumbnail"];
            setFieldsStickers(tags, stickerListAll);
            return tags;
        }
        case 'BrowseRadioFavorites':
        case 'BrowseRadioWebradiodb':
            return ["Added", "Country", "Description", "Genres", "Homepage", "Languages", "Last-Modified", "Name", "Region", "StreamUri", "Codec", "Bitrate", "Thumbnail"];
        case 'BrowseDatabaseTagList':
            return ["Value", "Thumbnail"];
        case 'BrowseDatabaseAlbumList':
        case 'QueueJukeboxAlbum': {
            const tags = settings.tagListAlbum.slice();
            if (tableName === 'QueueJukeboxAlbum') {
                tags.push('Pos');
            }
            tags.push('Thumbnail');
            if (settings.albumMode === 'adv') {
                tags.push('Discs', 'SongCount', 'Duration', 'Last-Modified');
                if (features.featDbAdded === true) {
                    tags.push('Added');
                }
            }
            setFieldsStickers(tags, stickerListAll);
            return tags.filter(function(value) {
                return value !== 'Disc';
            });
        }
        case 'BrowseDatabaseAlbumDetailInfo': {
            if (settings.albumMode === 'adv') {
                const tags = settings.tagListAlbum.slice();
                tags.push('Discs', 'SongCount', 'Duration', 'Last-Modified');
                if (features.featDbAdded === true) {
                    tags.push('Added');
                }
                return tags.filter(function(value) {
                    return value !== 'Disc' &&
                        value !== 'Album';
                });
            }
            else {
                return settings.tagListAlbum;
            }
        }
        // No Default
    }

    const tags = settings.tagList.slice();
    if (features.featTags === false) {
        tags.push('Title');
    }
    tags.push('Duration', 'Last-Modified', 'Filetype');
    if (tableName !== 'Playback') {
        tags.push('Thumbnail');
    }
    if (features.featDbAdded === true) {
        tags.push('Added');
    }

    switch(tableName) {
        case 'QueueCurrent':
            tags.push('AudioFormat', 'Priority');
            //fall through
        case 'BrowsePlaylistDetail':
        case 'QueueJukeboxSong':
            tags.push('Pos');
            break;
        case 'BrowseFilesystem':
            tags.push('Type', 'Filename');
            break;
        case 'Playback':
            tags.push('AudioFormat');
            if (features.featLyrics === true) {
                tags.push('Lyrics');
            }
            break;
        case 'QueueLastPlayed':
            tags.push('Pos', 'LastPlayed');
            break;
        // No Default
    }
    //sort tags 
    tags.sort();
    //append stickers
    setFieldsStickers(tags, stickerListSongs);
    return tags;
}

/**
 * Adds the sticker names to the fields array for songs
 * @param {Array} tags fields array to populate
 * @param {Array} stickers stickers array to add
 * @returns {void}
 */
function setFieldsStickers(tags, stickers) {
    if (features.featStickers === false) {
        return;
    }
    for (const sticker of stickers) {
        if (sticker === 'like' && features.featLike === false) {
            continue;
        }
        if (sticker === 'rating' && features.featRating === false) {
            continue;
        }
        tags.push(sticker);
    }
}

/**
 * Sets the data from the jsonrpc object to the dom node and
 * updates the jsonrpc object
 * @param {Element} entry Dom node representing the entry
 * @param {object} data Object data from jsonrpc response
 * @returns {void}
 */
function setEntryData(entry, data) {
    //set AlbumId
    if (data.AlbumId !== undefined) {
        setData(entry, 'AlbumId', data.AlbumId);
    }
    //and browse tags
    for (const tag of settings.tagListBrowse) {
        if (albumFilters.includes(tag) &&
            isEmptyTag(data[tag]) === false)
        {
            setData(entry, tag, data[tag]);
        }
    }
    //set Title to name if not defined - for folders and playlists
    if (data.Title === undefined) {
        data.Title = data.name;
    }

    //set Filetype
    if (data.Filetype === undefined) {
        data.Filetype = filetype(data.uri, false);
    }
    //set Thumbnail
    switch(data.Type) {
        case 'album':
            data.Thumbnail = getCssImageUri(data.FirstSongUri !== 'albumid'
                ? '/albumart-thumb?offset=0&uri=' + myEncodeURIComponent(data.FirstSongUri)
                : '/albumart-thumb/' + data.AlbumId);
            break;
        case 'song':
        case 'stream':
        case 'webradio':
            data.Thumbnail = getCssImageUri('/albumart?offset=0&uri=' + myEncodeURIComponent(data.uri));
            break;
        case 'dir': 
            data.Thumbnail = getCssImageUri('/folderart?path=' + myEncodeURIComponent(data.uri));
            break;
        case 'plist':
        case 'smartpls':
            data.Thumbnail = getCssImageUri('/playlistart?type=' + data.Type + '&playlist=' + myEncodeURIComponent(data.uri));
            break;
        case 'webradiodb':
            data.Thumbnail = getCssImageUri(data.Image);
            break;
        // No Default
    }
    if (data.Thumbnail !== undefined) {
        setData(entry, 'cssImageUrl', data.Thumbnail);
    }
    else {
        setData(entry, 'cssImageUrl', getCssImageUri('/assets/coverimage-notavailable'));
    }
}