1
0

Neu: ManhwaRead Chapter Downloader

This commit is contained in:
Akamaru
2025-11-13 21:25:40 +01:00
parent 4c7e7f40b5
commit 5b0bcb62b2
2 changed files with 373 additions and 0 deletions

View File

@@ -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`

View File

@@ -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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="display: inline-block; vertical-align: middle; margin-right: 4px;">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
`;
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);
}
}
})();