// ==UserScript== // @name YNJN Canvas Downloader // @namespace ynjn-downloader // @version 1.3 // @description Extrae páginas renderizadas en y las guarda como PNG en un ZIP (YNJN). // @match https://ynjn.jp/* // @require https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js // @require https://cdn.jsdelivr.net/npm/file-saver@2/dist/FileSaver.min.js // @grant none // @downloadURL none // ==/UserScript== (function() { 'use strict'; /** * Esperar a que la página termine de cargar */ function waitForPageLoad(callback) { if (document.readyState === 'complete') { callback(); } else { window.addEventListener('load', callback); } } waitForPageLoad(() => { console.log("🖼️ [YNJN Canvas Downloader] activo."); // Crear botón flotante para iniciar la descarga const downloadBtn = document.createElement('button'); downloadBtn.textContent = 'Descargar Canvas (YNJN)'; downloadBtn.style.cssText = ` position: fixed; top: 10px; right: 10px; z-index: 9999; background: #e63946; color: #fff; padding: 8px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; `; document.body.appendChild(downloadBtn); // Al hacer clic en el botón... downloadBtn.addEventListener('click', async () => { downloadBtn.disabled = true; downloadBtn.textContent = 'Cargando Canvas...'; // 1. Hacer scroll automático para forzar la carga de todos los await autoScroll(); // 2. Recoger los grandes const canvases = gatherCanvases(500, 500); // Ajusta 500, 500 según tus dimensiones mínimas deseadas if (!canvases.length) { alert('⚠️ No se encontraron grandes. Asegúrate de hacer scroll hasta el final.'); resetButton(); return; } // 3. Convertir cada a PNG y guardarlo en un ZIP try { await downloadCanvasAsZip(canvases); } catch (err) { console.error('❌ Error al descargar canvas:', err); alert('⚠️ Error al descargar. Revisa la consola (F12) para más detalles.'); } resetButton(); }); }); /** * Desplazamiento automático para cargar canvases (lazy load). */ async function autoScroll() { return new Promise(resolve => { let totalHeight = 0; const distance = 500; const timer = setInterval(() => { const scrollTopBefore = document.documentElement.scrollTop; window.scrollBy(0, distance); totalHeight += distance; // Si no avanzó más o llegamos al final, paramos if (document.documentElement.scrollTop === scrollTopBefore || (window.innerHeight + window.scrollY) >= document.body.offsetHeight) { clearInterval(timer); // Esperar 1 segundo extra por si hay lazy load setTimeout(resolve, 1000); } }, 400); }); } /** * Seleccionar todos los que tengan ancho y alto >= minWidth, minHeight * @param {number} minWidth - ancho mínimo * @param {number} minHeight - alto mínimo * @returns {HTMLCanvasElement[]} - array de elementos */ function gatherCanvases(minWidth, minHeight) { const allCanvas = Array.from(document.querySelectorAll('canvas')); // Filtrar solo los que tengan un tamaño natural grande return allCanvas.filter(cv => cv.width >= minWidth && cv.height >= minHeight); } /** * Convertir cada a PNG y guardar en un ZIP * @param {HTMLCanvasElement[]} canvasList */ async function downloadCanvasAsZip(canvasList) { const zip = new JSZip(); for (let i = 0; i < canvasList.length; i++) { const canvas = canvasList[i]; console.log(`🖼️ Procesando canvas (${i+1}/${canvasList.length}), tamaño: ${canvas.width}x${canvas.height}`); try { // Convertir canvas a Data URL PNG // OJO: Si el canvas está 'tainted' por CORS, lanzará error const dataURL = canvas.toDataURL('image/png'); // Convertir Data URL a Blob const blob = await (await fetch(dataURL)).blob(); // Guardar en el ZIP (ej: 001.png) const fileName = String(i+1).padStart(3, '0') + '.png'; zip.file(fileName, blob); } catch (err) { console.error('Error procesando un canvas:', err); } } // Generar el ZIP const zipContent = await zip.generateAsync({ type: 'blob' }); saveAs(zipContent, 'ynjn_canvas_pages.zip'); alert(`✅ Descarga completa: ${canvasList.length} páginas (canvas).`); } function resetButton() { const btn = document.querySelector('button'); if (btn) { btn.disabled = false; btn.textContent = 'Descargar Canvas (YNJN)'; } } })();