Source: viewsGrid.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 viewsGrid_js */

/**
 * Initializes a grid for drag and drop
 * @param {string} gridId grid id
 * @returns {void}
 */
function dragAndDropGrid(gridId) {
    const gridBody = elGetById(gridId);

    gridBody.addEventListener('dragstart', function(event) {
        const target = event.target.classList.contains('col')
            ? event.target
            : event.target.closest('.col');
        if (target === null) {
            return false;
        }
        event.target.classList.add('opacity05');
        // @ts-ignore
        event.dataTransfer.setDragImage(event.target, 0, 0);
        event.dataTransfer.effectAllowed = 'move';
        dragEl = target;
    }, false);

    gridBody.addEventListener('dragenter', function(event) {
        const target = event.target.classList.contains('col')
            ? event.target
            : event.target.closest('.col');
        if (target === null) {
            return false;
        }
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.add('dragover-left');
        }
    }, false);

    gridBody.addEventListener('dragleave', function(event) {
        const target = event.target.classList.contains('col')
            ? event.target
            : event.target.closest('.col');
        if (target === null) {
            return false;
        }
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.remove('dragover-left');
        }
    }, false);

    gridBody.addEventListener('dragover', function(event) {
        // prevent default to allow drop
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
        const target = event.target.classList.contains('col')
            ? event.target
            : event.target.closest('.col');
        if (target === null) {
            return false;
        }
        if (dragEl !== undefined &&
            dragEl.nodeName === target.nodeName)
        {
            target.classList.add('dragover-left');
        }
    }, false);

    gridBody.addEventListener('drop', function(event) {
        event.stopPropagation();
        event.preventDefault();
        if (dragEl === undefined ||
            dragEl.classList.contains('col') === false)
        {
            return;
        }
        const target = event.target.classList.contains('col')
            ? event.target
            : event.target.closest('.col');
        if (target === null) {
            return false;
        }
        target.classList.remove('dragover');
        const newPos = getData(target.firstElementChild, 'pos');
        const oldPos = getData(dragEl.firstElementChild, 'pos');
        if (oldPos === newPos) {
            return;
        }
        // set dragged element uri to undefined to force table row replacement
        setData(dragEl, 'uri', undefined);
        elHide(dragEl);
        // apply new order
        setUpdateViewId(gridId);
        switch(app.id) {
            case 'Home': {
                homeMoveIcon(oldPos, newPos);
                break;
            }
            case 'QueueCurrent': {
                queueMoveSong(oldPos, newPos);
                break;
            }
            case 'BrowsePlaylistDetail': {
                currentPlaylistMoveSong(oldPos, newPos);
                break;
            }
            // No Default
        }
    }, false);

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

/**
 * Replaces a grid col and tries to keep the selection state
 * @param {boolean} mode the selection mode
 * @param {HTMLElement} col col to replace
 * @param {HTMLElement} el replacement col
 * @returns {void}
 */
function replaceGridCol(mode, col, el) {
    const colCard = col.firstElementChild;
    const elCard = el.firstElementChild;
    if (getData(colCard, 'uri') === getData(elCard, 'uri')) {
        if (mode === true &&
            colCard.lastElementChild.lastElementChild.textContent === ligatures.checked)
        {
            elCard.lastElementChild.lastElementChild.textContent = ligatures.checked;
            elCard.classList.add('selected');
        }
        if (colCard.classList.contains('queue-playing')) {
            elCard.classList.add('queue-playing');
            elCard.style.background = colCard.style.background;
        }
    }
    col.replaceWith(el);
}

/**
 * Updates the grid from the jsonrpc response
 * @param {object} obj jsonrpc response
 * @param {string} list grid name to populate
 * @param {Function} [perCardCallback] callback per card
 * @param {Function} [createCardBodyCallback] callback to create the footer
 * @param {Function} [createCardActionsCallback] callback to create the footer
 * @returns {void}
 */
function updateGrid(obj, list, perCardCallback, createCardBodyCallback, createCardActionsCallback) {
    const grid = elGetById(list + 'List');
    let cols = grid.querySelectorAll('.col');
    const mode = grid.getAttribute('data-mode') === 'select'
        ? true
        : false;

    const footer = elCreateEmpty('div', {"class": ["card-footer", "card-footer-grid", "p-0", "d-flex", "justify-content-center"]});
    addActionLinks(footer);

    for (let i = 0; i < obj.result.returnedEntities; i++) {
        const card = elCreateEmpty('div', {"class": ["card", "card-grid", "clickable", "h-100"]});
        if (perCardCallback !== undefined &&
            typeof(perCardCallback) === 'function')
        {
            perCardCallback(card, obj.result.data[i], obj.result);
        }
        setEntryData(card, obj.result.data[i]);
        if (settings['view' + app.id].fields.includes('Thumbnail') &&
            obj.result.data[i].Thumbnail !== undefined)
        {
            card.appendChild(
                elCreateEmpty('div', {"class": ["card-title", "cover-loading", "cover-grid", "d-flex"]})
            );
            if (userAgentData.hasIO === true) {
                const observer = new IntersectionObserver(setGridImage, {root: null, rootMargin: '0px'});
                observer.observe(card);
            }
            else {
                card.firstChild.style.backgroundImage = obj.result.data[i].Thumbnail;
            }
        }
        const body = elCreateEmpty('div', {"class": ["card-body", "card-body-grid", "p-2"]});
        if (createCardBodyCallback !== undefined &&
            typeof(createCardBodyCallback) === 'function')
        {
            //custom body content
            createCardBodyCallback(body, obj.result.data[i], obj.result);
        }
        else {
            //default body content
            createGridBody(body, obj.result.data[i], list);
        }
        card.appendChild(body);
        if (createCardActionsCallback !== undefined &&
            typeof(createCardActionsCallback) === 'function')
        {
            //custom footer content
            const customFooter = elCreateEmpty('div', {"class": ["card-footer", "card-footer-grid", "p-0", "d-flex", "justify-content-center"]});
            createCardActionsCallback(customFooter, obj.result.data[i], obj.result);
            card.appendChild(customFooter);
        }
        else {
            //default footer content
            card.appendChild(footer.cloneNode(true));
        }
        const col = elCreateNode('div', {"class": ["col", "px-0", "mb-2", "flex-grow-0"]}, card);
        if (i < cols.length) {
            replaceGridCol(mode, cols[i], col);
        }
        else {
            grid.append(col);
        }
    }
    //remove obsolete cards
    cols = grid.querySelectorAll('.col');
    for (let i = cols.length - 1; i >= obj.result.returnedEntities; i--) {
        cols[i].remove();
    }

    unsetUpdateView(grid);
    setPagination(obj.result.totalEntities, obj.result.returnedEntities);
    setScrollViewHeight(grid);
    scrollToPosY(grid.parentNode, app.current.scrollPos);
}

/**
 * Returns the friendly names for type icons
 * @param {string} value tag value
 * @returns {string} friendly name
 */
function getTypeTitle(value) {
    switch(value) {
        case 'queue_music': return tn('Smart playlist');
        case 'list': return tn('Playlist');
        case 'folder_open': return tn('Folder');
        case 'music_note': return tn('Song');
        default: return value;
    }
}

/**
 * Populates the grid body
 * @param {Element} body grid body to populate
 * @param {object} data data to populate
 * @param {string} list view name
 * @returns {void}
 */
function createGridBody(body, data, list) {
    let i = 0;
    for (const tag of settings['view' + list].fields) {
        if (tag === 'Thumbnail') {
            i++;
            continue;
        }
        const value = printValue(tag, data[tag], data);
        const title = tag === 'Type'
            ? getTypeTitle(value.textContent)
            : value.textContent;
        body.appendChild(
            elCreateNode((i === 0 ? 'span' : 'small'), {"class": ["d-block"], "data-col": tag, "title": title},
                value
            )
        );
        i++;
    }
}

/**
 * Callback function for intersection observer to lazy load cover images
 * @param {object} changes IntersectionObserverEntry objects
 * @param {object} observer IntersectionObserver
 * @returns {void}
 */
function setGridImage(changes, observer) {
    changes.forEach(change => {
        if (change.intersectionRatio > 0) {
            observer.unobserve(change.target);
            const body = change.target.querySelector('.card-title');
            if (body) {
                body.style.backgroundImage = getData(change.target, 'cssImageUrl');
            }
        }
    });
}