355 lines
13 KiB
JavaScript
355 lines
13 KiB
JavaScript
// ==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);
|
|
}
|
|
}
|
|
})();
|