Popmundo Better Repertoire UI

Permite organizar o repertorio por grupos nomeados. Com funcionalidad de backup daquela banda e restauração de backup.

// ==UserScript==
// @name        Popmundo Better Repertoire UI
// @namespace   Violentmonkey Scripts
// @match       https://*.popmundo.com/World/Popmundo.aspx/Artist/Repertoire/*
// @exclude     /^https:\/\/.*\.popmundo\.com\/World\/Popmundo\.aspx\/Artist\/Repertoire\/[^/]+\/.+$/
// @author      Drinkwater
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @license     M.I.T
// @version     2.1
// @description Permite organizar o repertorio por grupos nomeados. Com funcionalidad de backup daquela banda e restauração de backup.

// ==/UserScript==

const currentUrl = window.location.href;
const bandId = currentUrl.split('/').pop();
const urlDomain = window.location.hostname;
let grupos = GM_getValue('grupos', []);
const style = document.createElement('style');
style.innerHTML = `
  .accordion-header {
        background-image: url(Images/bgr-item-header.png);
        background-color: #56686f;
        background-repeat: repeat-x;
        color: #fff;
        font-weight: 500;
        padding: 16px;
        cursor: pointer;
        border-radius: 8px;
        font-size: 12px;
        transition: background-color 0.3s ease, transform 0.2s ease;
    }

    /* Efeito hover */
    .accordion-header:hover {
        background-color: #3c4b51;
        transform: translateY(-2px);
        box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
    }
.accordion-content {
    max-height: 0;
    opacity: 0;
    overflow: hidden;
    padding: 0 10px;
    border: 1px solid #ccc;
    border-top: none;
    background-color: white;
    transition: max-height 0.4s ease, opacity 0.4s ease;
}

.accordion-content.open {
    max-height: 1000px; /* Defina uma altura máxima grande o suficiente */
    opacity: 1;
}

    .accordion-row {
        /* Define qualquer estilo adicional para as linhas accordion, se necessário */
    }
        .modal-dialog {
        width: 300px;
        height: auto;
    }
    .accordion-row td{
    padding: 5px 0px !important ;

}
.accordion-content tr{
    display: flex;
    justify-content: space-between;
}
.accordion-content tr td{
  max-width:90px;
}



`;

document.head.appendChild(style);

function createDeleteGroupModal() {
    const overlay = jQuery('<div>', { class: 'modal-overlay' }).css({
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        backgroundColor: 'rgba(0, 0, 0, 0.5)',
        zIndex: 999
    }).appendTo('body');

    const modal = jQuery('<div>', { class: 'modal-dialog' }).css({
        position: 'fixed',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        backgroundColor: '#fff',
        padding: '20px',
        borderRadius: '12px',
        boxShadow: '0 6px 16px rgba(0, 0, 0, 0.15)',
        zIndex: 1000,
        width: '320px',
        textAlign: 'center',
        fontFamily: 'Arial, sans-serif'
    }).appendTo('body');

    // Título do modal
    const modalTitle = jQuery('<h3>').text('Deletar Grupo').css({
        marginBottom: '15px',
        fontSize: '18px',
        color: '#333',
        fontWeight: 'bold'
    }).appendTo(modal);

    // Select para escolher o grupo
    const selectGroup = jQuery('<select>').css({
        marginBottom: '15px',
        padding: '5px',
        width: '100%',
        fontSize: '14px',
        borderRadius: '6px',
        border: '1px solid #ccc'
    }).appendTo(modal);

    // Exibir grupos que começam com o bandId, exceto o grupo "default", e mostrar sem o prefixo
    grupos.forEach(grupo => {
        if (grupo.startsWith(`${bandId}_`)) {
            selectGroup.append(jQuery('<option>', { value: grupo, text: grupo.replace(`${bandId}_`, '') }));
        }
    });

    // Botões de confirmação e cancelamento
    const confirmButton = jQuery('<button>').text('Confirmar').css({
        marginRight: '10px',
        padding: '8px 12px',
        backgroundColor: '#28a745',
        color: '#fff',
        border: 'none',
        borderRadius: '6px',
        cursor: 'pointer',
        fontSize: '14px',
    }).appendTo(modal);

    const cancelButton = jQuery('<button>').text('Cancelar').css({
        padding: '8px 12px',
        backgroundColor: '#dc3545',
        color: '#fff',
        border: 'none',
        borderRadius: '6px',
        cursor: 'pointer',
        fontSize: '14px'
    }).appendTo(modal);

    // Função para fechar o modal
    function closeModal() {
        modal.remove();
        overlay.remove();
    }

    // Evento de clique no botão de cancelamento
    cancelButton.on('click', function () {
        closeModal();
    });

    // Evento de clique no botão de confirmação
    confirmButton.on('click', function () {
        const selectedGroup = selectGroup.val();
        if (selectedGroup) {

            const tbody = jQuery('#tablesongs tbody');
            tbody.find('tr').each(function (index, row) {
                // Obter o grupo salvo para a música
                const currentGroup = GM_getValue(`${bandId}_song_${index}_group`);
                // Verifica se o grupo atual da música é o que está sendo deletado
                if (currentGroup === selectedGroup) {
                    GM_deleteValue(`${bandId}_song_${index}_group`);
                }
            });

            // Remove o grupo do GM_store
            grupos = grupos.filter(grupo => grupo !== selectedGroup);
            GM_setValue('grupos', grupos);

            alert(`Grupo "${selectedGroup.replace(`${bandId}_`, '')}" foi deletado`);
            closeModal();
            location.reload(); // Recarrega a página para atualizar os selects
        }
    });
}
jQuery(document).ready(function () {
    // Create a div for color pickers
    const colorPickerDiv = jQuery('<div>', { class: 'box', css: { marginBottom: '10px', padding: '10px', border: '1px solid #ccc' } });
    const heading = jQuery('<h2>').text('Configuração das Cores').appendTo(colorPickerDiv);

    // Create color pickers
    const propriaColorPickerLabel = jQuery('<label>', { css: { marginRight: '20px' } }).text('Musica própria: ').appendTo(colorPickerDiv);
    const propriaColorPicker = jQuery('<input>', { type: 'color', value: GM_getValue('propriaColor', '#000000') }).appendTo(propriaColorPickerLabel);

    const naoPropriaColorPickerLabel = jQuery('<label>').text('Comprada/Cover: ').appendTo(colorPickerDiv);
    const naoPropriaColorPicker = jQuery('<input>', { type: 'color', value: GM_getValue('naoPropriaColor', '#008000') }).appendTo(naoPropriaColorPickerLabel);
    const hr = jQuery('<hr>').appendTo(colorPickerDiv);

    // Create the submit button
    const submitButton = jQuery('<input>', { type: 'submit', value: 'Salvar Cores', class: 'cnf', css: { marginRight: '10px' } }).appendTo(colorPickerDiv);
    const createGroup = jQuery('<input>', { type: 'button', value: 'Criar novo grupo', class: 'cnf', css: { marginRight: '10px' } }).appendTo(colorPickerDiv);
    const deleteGroupButton = jQuery('<input>', { type: 'button', value: 'Deletar Grupos', class: 'cnf', css: { marginRight: '10px' } }).appendTo(colorPickerDiv);

    const createBackupButton = jQuery('<input>', { type: 'button', value: 'Criar Backup', class: 'cnf', css: { marginRight: '10px' } }).appendTo(colorPickerDiv);
    const restoreBackupButton = jQuery('<input>', { type: 'button', value: 'Restaurar Backup', class: 'cnf', css: { marginRight: '10px' } }).appendTo(colorPickerDiv);


    // Save the selected colors in GM storage when changed
    propriaColorPicker.on('change', function () {
        GM_setValue('propriaColor', propriaColorPicker.val());
    });

    naoPropriaColorPicker.on('change', function () {
        GM_setValue('naoPropriaColor', naoPropriaColorPicker.val());
    });

    // Handle the submit button click
    submitButton.on('click', function () {
        alert('Cores atualizadas com sucesso!');
        location.reload(); // Refresh the page
    });
    deleteGroupButton.on('click', function () {
        createDeleteGroupModal();
    });

    createGroup.on('click', function () {
        let groupName = prompt("Nome do grupo que deseja criar:");
        if (groupName) {
            const uniqueGroupName = `${bandId}_${groupName}`; // Prefixa o nome do grupo com o bandId
            grupos.push(uniqueGroupName); // Adiciona o novo grupo com o bandId
            GM_setValue('grupos', grupos); // Salva a lista de grupos atualizada
            alert(`Grupo "${uniqueGroupName}" criado com sucesso!`);
            location.reload(); // Recarrega a página para atualizar os selects
        }
    });

    // Insert the color picker div above the table
    jQuery('#tablesongs').before(colorPickerDiv);

    // Cria um iframe único que será reutilizado para cada link e o oculta
    const iframe = jQuery('<iframe>', {
        id: 'songIframe',
        width: '0px',
        height: '0px',
        css: { display: 'none' } // Deixa o iframe oculto inicialmente
    }).appendTo('#ppm-wrapper');

    // Seleciona todos os links da tabela
    const links = jQuery('#tablesongs tbody tr td a');
    let currentIndex = 0;

    // Função para processar um link
    function processLink() {
        if (currentIndex >= links.length) {
            console.log('Processamento concluído para todos os links.');
            iframe.remove(); // Remove o iframe após concluir o processamento de todos os links
            return;
        }

        const currentLink = jQuery(links[currentIndex]);

        // Verifica se o link contém uma imagem <img>, se contiver, pula para o próximo
        if (currentLink.find('img').length > 0) {
            currentIndex++;
            processLink();
            return;
        }

        const songName = currentLink.text(); // Get song name to display later
        const currentLinkHref = currentLink.attr('href');
        const songId = currentLinkHref.split('/').pop(); // Extract song ID from the URL

        const songData = GM_getValue(`${bandId}_${songId}`, null); // Use song ID as the key

        if (songData) {
            // If song data is already stored, display it directly
            displaySongData(songData, currentLink);
            currentIndex++;
            processLink();
        } else {
            const linkFormatted = "https://" + urlDomain + currentLinkHref;
            iframe.attr('src', linkFormatted);

            iframe.off('load').on('load', function () {
                const iframeContent = iframe.contents();
                let div1stBox = iframeContent.find('div.box').first();
                const bandHref = div1stBox.find('a[href^="/World/Popmundo.aspx/Artist/"]').attr('href');
                let bandaPropria = false;
                if (bandHref) {
                    const bandIdFromHref = bandHref.split('/').pop();
                    if (bandIdFromHref === bandId) {
                        bandaPropria = true;
                    }
                }

                let div2ndBox = iframeContent.find('div.box').eq(1);
                const melodiaTitle = div2ndBox.find('p a').eq(0).attr('title');
                const letraTitle = div2ndBox.find('p a').eq(1).attr('title');

                if (melodiaTitle && letraTitle) {
                    const notaMelodia = melodiaTitle.split('/')[0];
                    const notaLetra = letraTitle.split('/')[0];

                    const songData = {
                        songName,
                        notaMelodia,
                        notaLetra,
                        bandaPropria
                    };

                    // Save song data using the song ID as the key
                    GM_setValue(`${bandId}_${songId}`, songData);

                    displaySongData(songData, currentLink);
                } else {
                    console.error('Não foi possível encontrar os títulos para as notas da melodia e da letra.');
                }

                currentIndex++;
                processLink();
            });
        }
    }

    // Função para exibir os dados da música
    // Função para exibir os dados da música
    function displaySongData(songData, linkElement) {
        // Verifica se o ID do link é o da coluna de músicas, onde o ID contém 'lnkArtistSong'
        if (linkElement.attr('id') && linkElement.attr('id').includes('lnkArtistSong')) {
            let newText = `${songData.songName} (${songData.notaMelodia}/${songData.notaLetra})`;
            linkElement.text(newText);

            const propriaColor = GM_getValue('propriaColor', '#000000'); // Default green
            const naoPropriaColor = GM_getValue('naoPropriaColor', '#008000'); // Default yellow

            if (songData.bandaPropria) {
                linkElement.css({ 'color': propriaColor });
            } else {
                linkElement.css({ 'color': naoPropriaColor });
            }
        }
    }


    // Inicia o processamento do primeiro link
    processLink();



    // Função para criar o backup
    function exportBackup() {
        const backupData = {
            bandId: bandId,
            // Filtra os grupos que começam com o bandId atual
            grupos: grupos.filter(grupo => grupo.startsWith(`${bandId}_`)),
            musicas: {}
        };

        // Itera sobre os grupos filtrados para associar as músicas corretas
        backupData.grupos.forEach(grupo => {
            backupData.musicas[grupo] = [];
            const tbody = document.getElementById('tablesongs').querySelector('tbody');
            const rows = tbody.querySelectorAll('tr');
            rows.forEach(row => {
                const songLink = row.querySelector('a');
                if (!songLink) return;

                const songUrl = songLink.getAttribute('href');
                const songId = songUrl.substring(songUrl.lastIndexOf('/') + 1);

                const savedGroup = GM_getValue(`${bandId}_song_${songId}_group`, '');
                if (savedGroup === grupo) {
                    backupData.musicas[grupo].push({
                        songId: songId,
                        songName: songLink.textContent.trim()
                    });
                }
            });
        });

        const backupBlob = new Blob([JSON.stringify(backupData, null, 2)], { type: 'application/json' });
        const backupUrl = URL.createObjectURL(backupBlob);
        const downloadLink = document.createElement('a');
        downloadLink.href = backupUrl;
        downloadLink.download = `backup_band_${bandId}.json`;
        document.body.appendChild(downloadLink);
        downloadLink.click();
        document.body.removeChild(downloadLink);

        console.log("Backup criado com sucesso!");
    }

// Criar o elemento de entrada de arquivo persistente
const fileInput = jQuery('<input>', { type: 'file', accept: 'application/json', id: 'importBackupInput', style: 'display: none;' });
fileInput.appendTo(document.body);

// Associar o evento de mudança ao input de arquivo
fileInput.on('change', function (event) {
    const file = event.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (e) => {
        try {
            const backupData = JSON.parse(e.target.result);

            if (backupData.bandId !== bandId) {
                alert("O backup não corresponde ao Band ID atual!");
                return;
            }

            // Restaurar os grupos e músicas no GM Store
            grupos = backupData.grupos;
            GM_setValue('grupos', grupos);

            Object.entries(backupData.musicas).forEach(([grupo, musicas]) => {
                musicas.forEach(musica => {
                    GM_setValue(`${bandId}_song_${musica.songId}_group`, grupo);
                });
            });

            alert("Backup restaurado com sucesso!");
            location.reload(); // Recarregar a página para refletir as mudanças
        } catch (error) {
            console.error("Erro ao restaurar o backup:", error);
            alert("Erro ao restaurar o backup. Verifique o arquivo selecionado.");
        }
    };
    reader.readAsText(file);
});

    // Adicionando eventos aos botões
    createBackupButton.on('click', exportBackup);

    restoreBackupButton.on('click', () => {
        // Disparar o clique no input de arquivo
        document.getElementById('importBackupInput').click();
    });


    // Função para adicionar selectboxes em cada linha e salvar a seleção
    function addSelectBoxesToRows(tbody, accordionMap) {
        const rows = tbody.querySelectorAll('tr');
        rows.forEach((row) => {
            // Verifica se a linha não é um accordion (verificamos se a linha contém um link de música)
            const songLink = row.querySelector('a');
            if (!songLink || songLink.textContent.includes('Grupo')) {
                return; // Pula esta linha, pois é um accordion
            }
            if (row.classList.contains('accordion-row')) {
                return; // Se for um accordion, pula essa linha
            }

            // Extração do ID da música a partir do link
            const songUrl = songLink.getAttribute('href');
            const songId = songUrl.substring(songUrl.lastIndexOf('/') + 1);

            const songName = songLink.textContent.trim();
            const newCell = document.createElement('td');
            const select = createSelectBox(songId, accordionMap, row, songName);
            newCell.appendChild(select);
            row.appendChild(newCell);

            const savedGroup = GM_getValue(`${bandId}_song_${songId}_group`, ''); // Usando string vazia como padrão
            if (savedGroup && accordionMap[savedGroup]) {
                // Mover apenas músicas com grupos definidos (exceto default)
                accordionMap[savedGroup].appendChild(row);

                // Aplicar largura de 15% apenas nas colunas com `progressBar`
                row.querySelectorAll('td').forEach(td => {
                    if (td.querySelector('.progressBar')) {
                        td.style.width = '100%';
                    }
                });
            }
        });
    }

    // Função para adicionar o cabeçalho "Grupo" no thead
    function addTableHeader(thead) {
        const headerRow = thead.querySelector('tr');
        const newHeader = document.createElement('th');
        newHeader.textContent = 'Grupo';
        headerRow.appendChild(newHeader);
    }

    // Função para criar os accordions apenas para os grupos que começam com o bandId
    // Função para criar os accordions apenas para os grupos que começam com o bandId
    function createAccordions(tbody) {
        let accordionMap = {};

        // Filtra os grupos que começam com o bandId respectivo
        const filteredGroups = grupos.filter(grupo => grupo.startsWith(`${bandId}_`));

        // Referência para a primeira linha existente no tbody
        const firstRow = tbody.querySelector('tr');

        filteredGroups.forEach(grupo => {
            const accordionRow = document.createElement('tr');
            accordionRow.classList.add('accordion-row'); // Adiciona uma classe para identificar que é um accordion

            const accordionCell = document.createElement('td');
            accordionCell.setAttribute('colspan', tbody.closest('table').querySelectorAll('th').length);

            const accordionHeader = createAccordionHeader(grupo);
            const accordionContent = createAccordionContent();

            accordionHeader.addEventListener('click', () => {
                toggleAccordionContent(accordionContent);
            });

            accordionCell.appendChild(accordionHeader);
            accordionCell.appendChild(accordionContent);
            accordionRow.appendChild(accordionCell);

            // Insere o accordion no topo, antes da primeira linha
            tbody.insertBefore(accordionRow, firstRow);

            accordionMap[grupo] = accordionContent;
        });

        return accordionMap;
    }


    // Função para criar o cabeçalho clicável do accordion
    function createAccordionHeader(grupo) {
        const accordionHeader = document.createElement('div');
        accordionHeader.classList.add('accordion-header'); // Usa uma classe CSS para o cabeçalho do accordion

        // Exibe o grupo sem o prefixo bandId_ para visualização
        accordionHeader.textContent = `Grupo: ${grupo.replace(`${bandId}_`, '')}`;

        return accordionHeader;
    }

    // Função para criar o conteúdo do accordion
    function createAccordionContent() {
        const accordionContent = document.createElement('div');
        accordionContent.classList.add('accordion-content'); // Usa uma classe CSS para o conteúdo do accordion
        return accordionContent;
    }

    // Função para alternar a exibição do conteúdo do accordion
    function toggleAccordionContent(content) {
        if (content.classList.contains('open')) {
            content.classList.remove('open');
        } else {
            content.classList.add('open');
        }
    }

    function createSelectBox(songId, accordionMap, row, songName) {
        const select = document.createElement('select')
        select.style.maxWidth = '85px'; // Define o tamanho máximo


        // Adiciona a opção "Nenhum" para permitir que a música não pertença a nenhum grupo
        const noneOption = document.createElement('option');
        noneOption.value = ''; // Valor vazio para indicar "Nenhum"
        noneOption.textContent = 'Nenhum';
        select.appendChild(noneOption);

        // Filtra os grupos que começam com o bandId respectivo
        const filteredGroups = grupos.filter(grupo => grupo.startsWith(`${bandId}_`));

        // Cria as opções de select, exibindo o nome do grupo sem o prefixo bandId_
        filteredGroups.forEach(grupo => {
            const option = document.createElement('option');
            option.value = grupo; // Mantém o valor completo para salvar
            option.textContent = grupo.replace(`${bandId}_`, ''); // Remove o prefixo apenas para exibição
            select.appendChild(option);
        });

        // Define o grupo salvo ou a opção "Nenhum" como selecionada
        const savedGroup = GM_getValue(`${bandId}_song_${songId}_group`, ''); // Padrão é vazio ("Nenhum")
        select.value = savedGroup;

        select.addEventListener('change', () => {
            const selectedGroup = select.value;

            if (selectedGroup) {
                // Salva o grupo selecionado
                GM_setValue(`${bandId}_song_${songId}_group`, selectedGroup);
                console.log(`Salvando grupo: ${songName}, Grupo: ${selectedGroup}`);

                // Move a música para o grupo selecionado
                if (accordionMap[selectedGroup]) {
                    accordionMap[selectedGroup].appendChild(row);
                }
            } else {
                // Remove do grupo e retorna à tabela original
                GM_setValue(`${bandId}_song_${songId}_group`, '');
                console.log(`Removendo grupo da música: ${songName}`);

                // Move a linha de volta para o tbody original da tabela
                const originalTableBody = document.getElementById('tablesongs').querySelector('tbody');
                originalTableBody.appendChild(row);
            }
        });

        return select;
    }


    // Função principal que inicializa a tabela e seus componentes
    function initializeTable() {
        const table = document.getElementById('tablesongs');
        if (table) {
            const tbody = table.querySelector('tbody');
            const thead = table.querySelector('thead');
            if (thead) {
                addTableHeader(thead);
            }
            const accordionMap = createAccordions(tbody);
            addSelectBoxesToRows(tbody, accordionMap);
        }
    }

    // Função para remover as linhas que não foram atribuídas a nenhum accordion


    // Função principal que chama todas as outras
    function main() {
        initializeTable();
    }

    // Chamada da função principal
    main();
});