// ==UserScript== // @name ManhwaRead Chapter Downloader // @namespace https://git.ponywave.de/Akamaru/Userscripts // @version 1.0 // @description Lädt Chapter-Bilder von manhwaread.com als ZIP-Datei herunter // @author Akamaru // @match https://manhwaread.com/manhwa/*/chapter-* // @icon https://www.google.com/s2/favicons?domain=manhwaread.com&sz=32 // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/manhwaread-chapter-downloader.user.js // @downloadURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/manhwaread-chapter-downloader.user.js // ==/UserScript== (function() { 'use strict'; // Warte bis Seite geladen ist if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { // Warte bis chapterData verfügbar ist const checkAndAddButton = () => { // Prüfe ob wir auf einer Chapter-Seite sind if (!window.location.pathname.match(/\/manhwa\/.*\/chapter-/)) { return; } // Prüfe ob Button wirklich im DOM existiert (nicht nur die Variable) const existingBtn = document.getElementById('chapter-download-btn'); const buttonInDom = existingBtn && document.body.contains(existingBtn); if (window.chapterData && !buttonInDom) { addDownloadButton(); } }; // Initaler Check checkAndAddButton(); // Regelmäßiger Check alle 500ms um sicherzustellen, dass der Button da ist setInterval(checkAndAddButton, 500); // Beobachte DOM-Änderungen für SPA-Navigation (mit Throttling) let mutationTimeout; const observer = new MutationObserver(() => { clearTimeout(mutationTimeout); mutationTimeout = setTimeout(checkAndAddButton, 100); }); observer.observe(document.body, { childList: true, subtree: true }); // Überwache URL-Änderungen für SPA-Navigation // 1. History API überwachen (pushState/replaceState) const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { originalPushState.apply(this, arguments); setTimeout(checkAndAddButton, 100); setTimeout(checkAndAddButton, 500); setTimeout(checkAndAddButton, 1000); }; history.replaceState = function() { originalReplaceState.apply(this, arguments); setTimeout(checkAndAddButton, 100); setTimeout(checkAndAddButton, 500); setTimeout(checkAndAddButton, 1000); }; // 2. popstate Event für Browser-Navigation (Zurück/Vorwärts) window.addEventListener('popstate', () => { setTimeout(checkAndAddButton, 100); setTimeout(checkAndAddButton, 500); setTimeout(checkAndAddButton, 1000); }); } function addDownloadButton() { // Finde die Navigation const navContainer = document.querySelector('#readingNavTop .flex.items-center'); if (!navContainer) { return; } // Prüfe ob Button bereits existiert if (document.getElementById('chapter-download-btn')) { return; } // Erstelle Download-Button im Stil der Navigation const downloadBtn = document.createElement('button'); downloadBtn.id = 'chapter-download-btn'; downloadBtn.title = 'Download Chapter as ZIP'; downloadBtn.style.cssText = ` padding: 8px 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 6px; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; transition: opacity 0.2s; white-space: nowrap; `; // Icon + Text für den Button downloadBtn.innerHTML = ` `; downloadBtn.addEventListener('mouseover', () => { downloadBtn.style.opacity = '0.8'; }); downloadBtn.addEventListener('mouseout', () => { downloadBtn.style.opacity = '1'; }); downloadBtn.addEventListener('click', startDownload); // Füge Button am Ende der Navigation hinzu navContainer.appendChild(downloadBtn); } function getChapterInfo() { // Dekodiere chapterData if (!window.chapterData || !window.chapterData.data) { throw new Error('Chapter-Daten nicht gefunden'); } const decodedData = atob(window.chapterData.data); const images = JSON.parse(decodedData); const baseUrl = window.chapterData.base; // Extrahiere Manhwa- und Chapter-Namen aus URL const pathParts = window.location.pathname.split('/'); const manhwaName = pathParts[2]; // z.B. "room-of-guilty-pleasure" const chapterName = pathParts[3]; // z.B. "chapter-01" return { images: images.map(img => ({ url: `${baseUrl}/${img.src}`, width: img.w, height: img.h })), manhwaName, chapterName }; } function showProgress(message, percent = 0) { let progressOverlay = document.getElementById('chapter-download-progress'); if (!progressOverlay) { // Erstelle Progress-Overlay progressOverlay = document.createElement('div'); progressOverlay.id = 'chapter-download-progress'; progressOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center; `; const progressBox = document.createElement('div'); progressBox.style.cssText = ` background: #1a1d1f; color: #fff; padding: 40px; border-radius: 12px; min-width: 400px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); `; const title = document.createElement('h2'); title.textContent = 'Chapter wird heruntergeladen...'; title.style.cssText = 'margin: 0 0 20px 0; font-size: 24px; font-weight: bold;'; const progressBarContainer = document.createElement('div'); progressBarContainer.style.cssText = ` width: 100%; height: 30px; background: #2a2d31; border-radius: 15px; overflow: hidden; position: relative; margin-bottom: 15px; `; const progressBar = document.createElement('div'); progressBar.id = 'progress-bar'; progressBar.style.cssText = ` height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); width: 0%; transition: width 0.3s; `; const progressText = document.createElement('div'); progressText.id = 'progress-percent'; progressText.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-weight: bold; color: #fff; text-shadow: 0 2px 4px rgba(0,0,0,0.5); `; progressText.textContent = '0%'; progressBarContainer.appendChild(progressBar); progressBarContainer.appendChild(progressText); const statusText = document.createElement('div'); statusText.id = 'progress-status'; statusText.style.cssText = ` text-align: center; color: #ccc; font-size: 14px; `; statusText.textContent = message; progressBox.appendChild(title); progressBox.appendChild(progressBarContainer); progressBox.appendChild(statusText); progressOverlay.appendChild(progressBox); document.body.appendChild(progressOverlay); } else { const progressBar = document.getElementById('progress-bar'); const progressPercent = document.getElementById('progress-percent'); const progressStatus = document.getElementById('progress-status'); if (progressBar) { progressBar.style.width = percent + '%'; } if (progressPercent) { progressPercent.textContent = Math.round(percent) + '%'; } if (progressStatus) { progressStatus.textContent = message; } } } function hideProgress() { const progressOverlay = document.getElementById('chapter-download-progress'); if (progressOverlay) { progressOverlay.remove(); } } function showError(message) { hideProgress(); alert('Fehler beim Download: ' + message); } async function downloadImage(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Fehler beim Laden von ${url}`); } return await response.blob(); } async function startDownload() { try { showProgress('Initialisiere Download...', 0); // Hole Chapter-Informationen const { images, manhwaName, chapterName } = getChapterInfo(); showProgress(`Lade ${images.length} Bilder herunter...`, 10); // Erstelle ZIP const zip = new JSZip(); let downloadedCount = 0; // Download alle Bilder for (let i = 0; i < images.length; i++) { const image = images[i]; const pageNumber = String(i + 1).padStart(3, '0'); const extension = image.url.split('.').pop().split('?')[0]; // Entferne Query-Parameter try { const blob = await downloadImage(image.url); const fileName = `page_${pageNumber}.${extension}`; zip.file(fileName, blob); downloadedCount++; const progress = 10 + (downloadedCount / images.length) * 80; showProgress(`Bild ${downloadedCount}/${images.length} heruntergeladen...`, progress); } catch (err) { console.error(`Fehler beim Download von Bild ${i + 1}:`, err); throw err; } } if (downloadedCount === 0) { throw new Error('Keine Bilder konnten heruntergeladen werden.'); } showProgress('Erstelle ZIP-Datei...', 90); // Generiere ZIP const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); showProgress('Starte Download...', 95); // Erstelle Download-Link const url = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; a.download = `${manhwaName}_${chapterName}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showProgress(`Download abgeschlossen! ${downloadedCount} Bilder heruntergeladen.`, 100); // Schließe Progress nach 2 Sekunden setTimeout(hideProgress, 2000); } catch (error) { console.error('Download-Fehler:', error); showError(error.message); } } })();