Neu: ManhwaRead Chapter Downloader
This commit is contained in:
19
README.md
19
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`
|
||||
|
||||
354
manhwaread-chapter-downloader.user.js
Normal file
354
manhwaread-chapter-downloader.user.js
Normal 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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user