Greasy Fork

Greasy Fork is available in English.

Rumanhua Downloader V12.6

Tương thích hoàn toàn với link http://rumanhua2.com trên điện thoại.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Rumanhua Downloader V12.6
// @namespace    rumanhua-downloader
// @version      12.6
// @description  Tương thích hoàn toàn với link http://rumanhua2.com trên điện thoại.
// @author       User
// @match        *://rumanhua2.com/*
// @match        *://*.rumanhua2.com/*
// @match        http://rumanhua2.com/*
// @match        http://*.rumanhua2.com/*
// @include      /^http?://(www\.|m\.)?rumanhua2\.com/.*$/
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/npm/@zip.js/[email protected]/dist/zip.min.js
// ==/UserScript==

(function () {
    'use strict';

    // Xác nhận script chạy (Kiểm tra trong phần Console nếu cần)
    console.log("Rumanhua Script Started");

    const PARALLEL_DOWNLOADS = 12; // Số luồng vừa phải cho điện thoại

    // --- GIAO DIỆN MOBILE ---
    const ui = document.createElement('div');
    // Đẩy lên 130px để chắc chắn nằm trên thanh địa chỉ/công cụ của Firefox
    ui.style = "position: fixed; right: 15px; bottom: 130px; z-index: 2147483647;";
    document.body.appendChild(ui);

    const btn = document.createElement('button');
    btn.innerHTML = '🚀 TẢI ZIP';
    btn.style = "padding: 16px 22px; background: #e67e22; color: white; border: 2px solid #fff; border-radius: 50px; font-weight: bold; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.5); font-size: 15px; font-family: sans-serif;";
    ui.appendChild(btn);

    const status = document.createElement('div');
    status.style = "position: fixed; top: 15%; left: 10%; right: 10%; background: #fff; color: #e67e22; padding: 15px; border-radius: 10px; border: 2px solid #e67e22; font-weight: bold; font-size: 15px; display: none; z-index: 2147483647; text-align: center; box-shadow: 0 5px 20px rgba(0,0,0,0.3);";
    document.body.appendChild(status);

    const log = (txt) => { status.style.display = 'block'; status.innerHTML = txt; };

    function getCleanFileName() {
        try {
            let fullTitle = document.title;
            // Rumanhua mobile title fallback
            let mangaName = fullTitle.match(/《(.+?)》/)?.[1] || "Manga";
            let chapterName = fullTitle.split('》')[1]?.split('-')[0]?.trim() || fullTitle.split('_')[0] || "Chapter";
            return `${mangaName}_${chapterName}`.replace(/[\\/:*?"<>|]+/g, "_").replace(/\s+/g, "_").trim();
        } catch (e) { return "Rumanhua_" + Date.now(); }
    }

    async function fetchImage(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET', url: url, responseType: 'arraybuffer',
                timeout: 25000,
                onload: (res) => resolve(res.response),
                onerror: (err) => reject(err)
            });
        });
    }

    btn.onclick = async () => {
        const imgTags = document.querySelectorAll('img');
        let urls = [];
        imgTags.forEach(img => {
            const src = img.getAttribute('data-src') || img.getAttribute('data-original') || img.getAttribute('v-lazy') || img.src;
            if (src && src.match(/\.(jpg|jpeg|png|webp|avif)/i) && !src.includes('logo')) {
                try {
                    urls.push(new URL(src, location.href).href);
                } catch(e){}
            }
        });
        urls = [...new Set(urls)];

        if (urls.length === 0) {
            alert("❌ Không thấy ảnh. Bạn cần kéo xuống cuối chương để nạp ảnh trước!");
            return;
        }

        btn.disabled = true;
        btn.style.opacity = '0.5';
        log("⚡ Đang chuẩn bị nạp...");

        const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"));
        let completed = 0;
        let index = 0;

        const worker = async () => {
            while (index < urls.length) {
                const i = index++;
                const url = urls[i];
                try {
                    const data = await fetchImage(url);
                    const ext = url.split('.').pop().split('?')[0] || 'jpg';
                    const name = `${String(i + 1).padStart(3, '0')}.${ext}`;
                    await zipWriter.add(name, new zip.Uint8ArrayReader(new Uint8Array(data)), { level: 0 });
                } catch (e) {}
                completed++;
                log(`📥 Đang nạp: ${completed} / ${urls.length}`);
            }
        };

        const workers = [];
        for (let i = 0; i < Math.min(PARALLEL_DOWNLOADS, urls.length); i++) {
            workers.push(worker());
        }
        await Promise.all(workers);

        log("📦 Đang đóng gói ZIP...");
        const zipBlob = await zipWriter.close();
        const a = document.createElement("a");
        a.href = URL.createObjectURL(zipBlob);
        a.download = `${getCleanFileName()}.zip`;
        document.body.appendChild(a);
        a.click();
        
        log("✅ HOÀN TẤT! Đã gửi lệnh tải.");
        btn.disabled = false;
        btn.style.opacity = '1';
        setTimeout(() => { status.style.display = 'none'; a.remove(); }, 5000);
    };
})();