diff --git a/README.md b/README.md index 365d561..129db9f 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,25 @@ Lädt Cover von MangaDex im Bulk als ZIP-Datei herunter. Perfekt um Manga-Cover --- +### 13. `manhwaread-chapter-downloader.user.js` +**[Installieren](https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/manhwaread-chapter-downloader.user.js)** + +**Beschreibung:** +Lädt Chapter-Bilder von manhwaread.com als ZIP-Datei herunter. Perfekt um ganze Chapter für Offline-Lesen zu archivieren. + +**Funktionen:** +- Download-Button direkt in der oberen Navigation integriert +- Automatische Erkennung und Dekodierung der Chapter-Daten +- Lädt alle Bilder eines Chapters herunter +- Bilder werden fortlaufend nummeriert: `page_001.jpg`, `page_002.jpg`, etc. +- Fortschrittsanzeige während des Downloads (mit Prozentangabe und Status) +- Automatischer ZIP-Download mit Manhwa-Titel und Chapter im Dateinamen (z.B. `room-of-guilty-pleasure_chapter-01.zip`) +- Persistent: Button bleibt auch bei Chapter-Navigation sichtbar +- JSZip Integration für effiziente ZIP-Kompression +- Fehlerbehandlung für fehlgeschlagene Downloads + +--- + ## Übersicht der enthaltenen UserStyles ### 1. `myanimelist-tweaks.user.css` diff --git a/manhwaread-chapter-downloader.user.js b/manhwaread-chapter-downloader.user.js new file mode 100644 index 0000000..6eb70d5 --- /dev/null +++ b/manhwaread-chapter-downloader.user.js @@ -0,0 +1,354 @@ +// ==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); + } + } +})();