Viz Manga Universal Downloader v10.2.1

Descarga capítulos completos de Viz Manga y Shonen Jump, permitiendo seleccionar imágenes y recomponer imágenes en forma de rompecabezas.

// ==UserScript==
// @name         Viz Manga Universal Downloader v10.2.1
// @namespace    shadows
// @version      10.2.1
// @description  Descarga capítulos completos de Viz Manga y Shonen Jump, permitiendo seleccionar imágenes y recomponer imágenes en forma de rompecabezas.
// @author       
// @license      MIT
// @match        https://www.viz.com/vizmanga/*
// @match        https://www.viz.com/shonenjump/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @connect      viz.com
// ==/UserScript==
"use strict";

/**
 * Nota:
 * - Se asume que las imágenes de manga se encuentran en etiquetas <img>.
 * - Si el sitio utiliza <canvas> u otra técnica (por ejemplo, fragmentos reordenados o encriptados),
 *   se incluye la opción de recomponer la imagen “rompecabezas”. Ajusta los selectores según el caso.
 */

(function() {
    // --- Crear panel de opciones flotante ---
    const panel = document.createElement('div');
    panel.style.position = 'fixed';
    panel.style.top = '10px';
    panel.style.right = '10px';
    panel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
    panel.style.color = '#fff';
    panel.style.padding = '10px';
    panel.style.zIndex = '10000';
    panel.style.borderRadius = '5px';
    panel.style.fontSize = '13px';
    panel.style.maxWidth = '300px';
    panel.style.lineHeight = '1.4';
    panel.innerHTML = "<strong>Viz Manga Downloader</strong><br>";

    // Botón: Descargar Capítulo (todas las imágenes en <img>)
    const downloadChapterButton = document.createElement('button');
    downloadChapterButton.textContent = 'Descargar Capítulo';
    downloadChapterButton.style.margin = "3px";
    panel.appendChild(downloadChapterButton);

    // Botón: Auto-Descargar (activar/desactivar con IntersectionObserver)
    const autoDownloadToggle = document.createElement('button');
    autoDownloadToggle.textContent = 'Auto-Descargar: OFF';
    autoDownloadToggle.style.margin = "3px";
    panel.appendChild(autoDownloadToggle);

    // Botón: Actualizar lista de imágenes (selección manual)
    const updateListButton = document.createElement('button');
    updateListButton.textContent = 'Actualizar lista';
    updateListButton.style.margin = "3px";
    panel.appendChild(updateListButton);

    // Botón: Descargar seleccionados
    const downloadSelectedButton = document.createElement('button');
    downloadSelectedButton.textContent = 'Descargar seleccionados';
    downloadSelectedButton.style.margin = "3px";
    panel.appendChild(downloadSelectedButton);

    // Botón: Recomponer Imagen Puzzle
    const reassemblePuzzleButton = document.createElement('button');
    reassemblePuzzleButton.textContent = 'Recomponer Imagen Puzzle';
    reassemblePuzzleButton.style.margin = "3px";
    panel.appendChild(reassemblePuzzleButton);

    // Contenedor para la lista de imágenes con checkboxes
    const imageListContainer = document.createElement('div');
    imageListContainer.style.maxHeight = '200px';
    imageListContainer.style.overflowY = 'auto';
    imageListContainer.style.marginTop = '5px';
    panel.appendChild(imageListContainer);

    document.body.appendChild(panel);

    // --- Función para descargar una imagen usando GM_download ---
    function descargarImagen(url, nombre) {
        GM_download({
            url: url,
            name: nombre,
            onerror: err => console.error('Error al descargar', url, err),
            onload: () => console.log('Descargado:', url)
        });
    }

    // --- Botón: Descargar Capítulo ---
    downloadChapterButton.addEventListener('click', function() {
        const images = document.querySelectorAll('img');
        let count = 0;
        images.forEach((img, index) => {
            if (img.src) {
                const nombre = `page-${index + 1}.jpg`;
                descargarImagen(img.src, nombre);
                count++;
            }
        });
        alert(`Iniciada descarga de ${count} imágenes.`);
    });

    // --- Auto-Descargar usando IntersectionObserver ---
    let autoDownloadEnabled = false;
    autoDownloadToggle.addEventListener('click', function() {
        autoDownloadEnabled = !autoDownloadEnabled;
        autoDownloadToggle.textContent = `Auto-Descargar: ${autoDownloadEnabled ? 'ON' : 'OFF'}`;
    });

    const descargadas = new Set(); // Para evitar descargas duplicadas

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting && autoDownloadEnabled) {
                const img = entry.target;
                if (img.src && !descargadas.has(img.src)) {
                    descargadas.add(img.src);
                    // Nombre personalizado (timestamp)
                    const nombre = `auto-${Date.now()}-${Math.random().toString(36).substr(2,6)}.jpg`;
                    descargarImagen(img.src, nombre);
                }
            }
        });
    }, { threshold: 0.5 });

    function observarImagenes() {
        document.querySelectorAll('img').forEach(img => {
            observer.observe(img);
        });
    }
    observarImagenes();

    // Si se agregan imágenes dinámicamente, se vuelven a observar
    const mutationObserver = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.tagName === 'IMG') {
                        observer.observe(node);
                    } else {
                        node.querySelectorAll('img').forEach(img => observer.observe(img));
                    }
                }
            });
        });
    });
    mutationObserver.observe(document.body, { childList: true, subtree: true });

    // --- Actualizar lista de imágenes para selección manual ---
    updateListButton.addEventListener('click', function() {
        imageListContainer.innerHTML = ''; // Limpiar lista actual
        const images = document.querySelectorAll('img');
        images.forEach((img, index) => {
            if (img.src) {
                const label = document.createElement('label');
                label.style.display = 'block';
                label.style.marginBottom = '3px';
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.checked = true;
                checkbox.dataset.url = img.src;
                label.appendChild(checkbox);
                label.appendChild(document.createTextNode(` Imagen ${index + 1}`));
                imageListContainer.appendChild(label);
            }
        });
    });

    // --- Descargar imágenes seleccionadas ---
    downloadSelectedButton.addEventListener('click', function() {
        const checkboxes = imageListContainer.querySelectorAll('input[type="checkbox"]');
        let count = 0;
        checkboxes.forEach((checkbox, index) => {
            if (checkbox.checked) {
                const url = checkbox.dataset.url;
                const nombre = `selected-${index + 1}.jpg`;
                descargarImagen(url, nombre);
                count++;
            }
        });
        alert(`Iniciada descarga de ${count} imágenes seleccionadas.`);
    });

    // --- Recomponer Imagen Puzzle ---
    reassemblePuzzleButton.addEventListener('click', async function() {
        await reassemblePuzzleImage();
    });

    async function reassemblePuzzleImage() {
         // Se asume que el contenedor de la imagen puzzle tiene la clase 'puzzle-container'
         const container = document.querySelector('.puzzle-container');
         if (!container) {
             alert("No se encontró el contenedor de la imagen puzzle. Asegúrate de que el contenedor tenga la clase 'puzzle-container'.");
             return;
         }
         // Obtener dimensiones del contenedor
         const containerRect = container.getBoundingClientRect();
         const width = containerRect.width;
         const height = containerRect.height;

         // Crear canvas con dimensiones del contenedor
         const canvas = document.createElement('canvas');
         canvas.width = width;
         canvas.height = height;
         const ctx = canvas.getContext('2d');

         // Buscar elementos dentro del contenedor que tengan un background-image (fragmentos)
         const fragments = container.querySelectorAll('*');
         const fragmentElements = Array.from(fragments).filter(el => {
              const bg = window.getComputedStyle(el).backgroundImage;
              return bg && bg !== 'none';
         });
         if (fragmentElements.length === 0) {
             alert("No se encontraron fragmentos de imagen con background-image dentro del contenedor.");
             return;
         }

         // Asumir que todos los fragmentos usan la misma imagen base.
         const firstBg = window.getComputedStyle(fragmentElements[0]).backgroundImage;
         const urlMatch = firstBg.match(/url\(["']?(.*?)["']?\)/);
         if (!urlMatch) {
             alert("No se pudo extraer la URL de la imagen del primer fragmento.");
             return;
         }
         const imageUrl = urlMatch[1];
         let baseImage;
         try {
             baseImage = await loadImage(imageUrl);
         } catch (err) {
             alert("Error al cargar la imagen base del puzzle.");
             console.error(err);
             return;
         }

         // Para cada fragmento, obtener la posición del background y sus dimensiones
         fragmentElements.forEach(el => {
             const style = window.getComputedStyle(el);
             const pos = style.backgroundPosition.split(' ');
             // Se asume que la posición viene en píxeles (ej: "-100px -50px")
             let posX = parseFloat(pos[0]);
             let posY = parseFloat(pos[1]);
             // Obtener dimensiones del fragmento
             const fragRect = el.getBoundingClientRect();
             const fragWidth = fragRect.width;
             const fragHeight = fragRect.height;
             // Posición relativa del fragmento respecto al contenedor
             const containerRectLocal = container.getBoundingClientRect();
             const offsetX = fragRect.left - containerRectLocal.left;
             const offsetY = fragRect.top - containerRectLocal.top;

             // Dibujar la porción correspondiente de la imagen base en el canvas.
             // Se utiliza Math.abs para convertir valores negativos a positivos.
             ctx.drawImage(
                 baseImage,
                 Math.abs(posX),
                 Math.abs(posY),
                 fragWidth,
                 fragHeight,
                 offsetX,
                 offsetY,
                 fragWidth,
                 fragHeight
             );
         });

         // Convertir el canvas a un blob y descargar la imagen recompuesta
         canvas.toBlob(blob => {
             const blobUrl = URL.createObjectURL(blob);
             GM_download({
                 url: blobUrl,
                 name: 'puzzle_recomposed.png',
                 onerror: err => console.error("Error en la descarga de la imagen puzzle recompuesta:", err),
                 onload: () => {
                     console.log("Imagen puzzle recompuesta descargada correctamente.");
                     URL.revokeObjectURL(blobUrl);
                 }
             });
         });
    }

    // Función auxiliar para cargar una imagen (devuelve una promesa)
    function loadImage(url) {
         return new Promise((resolve, reject) => {
             const img = new Image();
             img.crossOrigin = "Anonymous";
             img.onload = () => resolve(img);
             img.onerror = err => reject(err);
             img.src = url;
         });
    }
})();