☰

DoujinDesu Downloader

Download manga + WebP/JPG + ZIP

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램을 먼저 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         DoujinDesu Downloader
// @namespace    https://doujin.desu.xxx/
// @version      2.6
// @description  Download manga + WebP/JPG + ZIP
// @author       qxyraxp
// @license      MIT
// @match        https://doujin.desu.xxx/*
// @match        http://doujin.desu.xxx/*
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// ==/UserScript==
(function () {
    'use strict';
    let imageURLs = [];
    let panelOpen = false;
    let autoScanInterval = null;

    // Base64 icons (circular 32x32)
    const ICON_FLOAT = 'data:image/png;base64,' + `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`;
    const ICON_SELECTED = 'data:image/png;base64,' + `iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAHdElNRQfqCgsJDDBjB0nLAAALiUlEQVRYwy2WyY+cd0JA32/51tqrq6qrq1f34m5vsTOOHTsTJ5MJCUgkMGgOCCFxRgIkrtz6wB+AkGAuDGgOSAgFSEYzI41HGZGIYTQk2I7jJe623XbavVV11758+8ch/APv8A5PT8ycOpsmcUIQBERhTJqmKC2RSqKkQmuBoSRKa7TSSK1QlolhmkjDBKFRSqENg1qxQC2fw1IKQ6Rk5tbojHziXpek1yEe9lCVGuSLtFvH+J6PDOMYPwiJwxjSFCkECokUEgQIBFoplFYIy0AZCjMO0IMeHB2SHO4RtZtkIp+8glRJ3FwW7WSoNRqcKueoZSwqc3Mo04Kv7mJqiWXbAOh4PEKmklQp7DTBEpJASVIhESRYhkZYFoY3Ids8wBmPkBMPU0tMx0GZJqAQBwccPlCU6zOUz25ArYFrauz5BYxqHT+M8ApT7Nsu0d4+2na+sb2M3FyaDMinCYFp4ilFFgiRGIYGQ1HutJk5PMAdDrDSlHq9iq80KjWQPqSTCCYBBSQ5AUG3h6UltqFQbg5p2sgkwQQ83wdtEEUxnuehrqRi005jnCBgTiqqWZdBFIHtIC2bmcN96u0WGkEgBaV6BSlN4u4YgoQ0TZBC4JMgpGQShCRpgvA8Em9CZ9DGlIpcvYHrmHQ6HbwwJE1TJuMxal1am0IIpJT0vRGv1yq8VCzwQCqmm01qvRPQBoZUWLUS3igibo8wtEBJQUYpEpHgmgZTUuOSkvgBSRiRBSypwB8TxzGWm8F0HIbDIZ7n4Xs+WgiNIRMyhkHDLREGEasli/NHuyTdNqE20ICsFAgmCeY4JuOYmEJCmuLHEXltktGaJEmI4wSLFOl7DI5bZAHXMCgO2mQL5zjxA6ana4RhSK/XQ7qGwlYakcJSscxwOOThwT5Bv00qwU8SgryDH6XoUYhrKTSghEBKQcG2MaVkGAREcYySAkdL3DRFTXwYDmlIkMctWg/uorSmUCxSLpWxbRt11c5u+nHIUTDicadDT0v2o4BhxsG5eJaxSjHGHmock7MMTCmIk4QoTYiTGCUkErBEShDHjNMYxzZBKJSSVEt54jih2xuSL2SIpxtIbSCVxA98lJsmm53QwykWqJ1exa5VmJ6dxVlZYmptlXfef4/xXovgqEUplyH0A4QQxGmCIRUyTckKUAoQKUIqlNZAShQnHHe6JFJSyWeZ3ZjHy5aYasxTLhUxTQttpzC2LRprpzG0pjXsIUhpTFdZPXWKmWye/rNdZmoVwolP1nWZeBNAEQMVrVBCgBJYWjNOIaMlWikEAsPIIKOQly6tULwwz/adHeor69hWlonnIzNxylxjFk8JgiShXq0ylXN5bbnAjQXNp//0QzJxjKE1RDGGEBhSEQtB0dS4+psUZy0LhMSSCpGATsE0FA4pp199mfIrF/G6XWoZgydbTxj0eqRxjKoLudl3LaqlEqYUnDp3jt2DXUaWzbEX0Xy0S8F0mPT6WFIgkphBEBIaiko2j7JscobCKOUYa42dxEgl0abBJI5w83n6nQ5bv7lNuVolyRbYPhoQhgGt4zbqFdvdHIcxk8ocT5ttJgS8duM6Fcfg409uYaQuhSQkCUMskRBEIWGhwMLGKkGryVAKojhh/a1vk/gBUa+Hm3GJkhhNSjGXozxdJw4jnMGYoe/TypSx3Sy90Rg5rTQLSYTneSxfuMB71y/z+pVXsE3J75xZYsZIiZUmX8gjo5hIQpI3yRqKxUadSj6DmXMYmwaxF4JlE0YhZ2amuLKxjBNGHBzuc/bMOgUkc7ZF9clDukd7BEGAPEoSDiYe+Z17XEt7bH12h3/74b/wPzdvs333GUJJstNlSGNkmqKmyviTCQ/u3OXI0HiGYmXjNPWZGbxgQhIFTLkWZ5dmGVg251++gGVpBv0B82fPIlJwDndxmsekpoM8bVksZ3NUAsGtH3/Mf+56fBjPcrizR851iZWkUK9CmmBKKFbL/P7Fs/z5u9fJZm2GUyVKayvUGnWMahmtYGWhQWpaDEyTkYhpLC3SEhKnVGQsNDfOnudSNs/2s130LcviQhRjGTHi1AbdhQ06ccjsbI3yVIXdYQ+3VMDNZwk7HerZPHM3blBSPm+nJmZjEcMPKJXLnLt+ld0kxZ2fZzAe07hwnsx4xN6zPaazWZSQ+JMJpWKF7kkbHRjoL4wcYyPid5MRSalK1s2gewcEmQzOVJnjp9tcuLhOdabK/pOn5C2TubV14iCilETYhoGwbeIw4NobNyjWavzkXz8gFwT4+we8sr5G4nlYhker0+aw2+W/nuzgWXl07RR6sV7mSavPx67Jm45FkMRIw+L27glnfJ/Z1VXazWMa1TJBPo9x1IQ4xjUVaSIBgRCClBSnXGHxoskf7H+FMbfONppPf/QjTocJg+kQvdBAztWxmyfctPJEhoO6empps1HN88WLDk1SZLFC72SfXueQ5lGLq/VpLCdLnCtw7upl0v09+nFMZXkZU2uk+P93lAphmVQ3zjJ1/iV0fZZirc5o6xGVyYRGucyxYfAiiViqLbDllOkGEerSyXhzY2ORbMbiy6cv2Ds5oICHNxrQBvb2Dsjcf4j0PC59/3vMf+dN7EIOJ5vFtCyEUihDow2FWZtG2g7CyVOYmeXmzU+pnZww5dgoBK18Bhybw4VVtvsRiechczJBfX6fqm2yvLrIyfMtLJ1yem2FyBvzVRzxM9dhe+cpe/ceYNoOMyur2LkswjDQtkWUpqS2jVksEqcphmuxfecOD28/otyY5dTZczzNZ/hld8SL1EJli3jDEVJLpGOa2FKQ+3IL0/cwLIPGTJXR0MMwHFJvxEG/z+SNd9jzQg6f7+D5EwzbRpoGQmvcfA67XicmRdsmKMkHf/sDxpMxa4uznBRyVF67xnvXrpB+fchn95+hTQNSUNec7KbjmDjAYavJo3BMOI5ongxIhcTJ2Fz61nW++4d/zFTGxBn3GQ6GuPkiTr7wDcRxMQp5tGMxHI75+7/+Gz77wd8x/513+PV/3+L2Rx/SX1jnyY9vMjUCbSn6tkUYgW77PtXEwTYMno/7TFs26ysbdIOI7nETyzYxyzX6oxGVfgfpmoTehJ17X1BbWWN6fhHDNun1e2w/avOTT+5y/95TKpZDd/85USZH47iN9uHO+iVWXuyz0OrSfPGM7tI51KlEbHoCDNvm5ydN5qt1ZDYLSuLaJnNTGRw8fvYf/85ry3OsLcyTBAEZUxOPR+w1m2xNYu7vdXjSCTjuDPA7J2wMTmgdPWNv0qEyCXF7XV4sbxAXCpRHAX7/hL3iFGo9ZTNZWOJX3Q7NwOfcy1cwXBdpZ0E7NI/7ZDQsztZ49PQ5iecThBGfP9gi0BZPv97lp7cfcfnGmwR+QPukS//xFtdESCQlOzpCv2hRCz2YW2JUqxN1ewweP6Q/u4h2nAzu0ikePX7Ia2+8xczCMl/vH5LECa3drzEl7LwY8vbrlzk0x/zVP/4zWcPGVIKV2Tq6Os/W08dEiea7v/0usTaxR31m8lnSicEtMySTy9JpneA/vEO7WGWquUNy8IwkjlGvrqxvfj4eEMYxV698m7xTJG/nWJiqUcxkebz1EClSJmFMECSsnV6j1W7xvbfeoJLJECRwNEnotfbxRkPKy2ewTg55VYRY2qY1XeKl2XnyO/toLVnXCeOff4RaWqW9eg61cv7S5q/u/i+/9/73efRwm0df3uMv/vQvSdKUX/zip3TbJyhtkAiDVChIBKZjs733NX/2J3/E+2+/yWcvuiShT+L7HDVbnMHnVVNADEcqxXFd3l1cJQNU/TGRm+Pz9W8ROy6q4082K/kiFy9f5zeffkKv3+H58x3u3/uCJ0++IlcsUZqq4sqUjAkn/R6FfBE3n+cfPviQt65f4bcurvPRzZuYMiE82KE/8TkMUuw0pbE4x5PWEasRTJ85zdiw+OWZy+xKA+F7/B+WmRsN6W6DNAAAAABJRU5ErkJggg==`;
    const ICON_ZIP = 'data:image/png;base64,' + `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`;

    // ==================== Minimal ZIP ====================
    function crc32(buf) {
        let c = ~0;
        for (let i = 0; i < buf.length; i++) {
            c ^= buf[i];
            for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
        }
        return ~c >>> 0;
    }
    function createZip(files) {
        const localHeaders = [], fileDatas = [];
        let offset = 0;
        for (const f of files) {
            const nameBytes = new TextEncoder().encode(f.name);
            const data = f.data;
            const crc = crc32(data);
            const local = new Uint8Array(30 + nameBytes.length);
            const v = new DataView(local.buffer);
            v.setUint32(0, 0x04034b50, true);
            v.setUint16(4, 20, true);
            v.setUint16(8, 0, true);
            v.setUint32(14, crc, true);
            v.setUint32(18, data.length, true);
            v.setUint32(22, data.length, true);
            v.setUint16(26, nameBytes.length, true);
            local.set(nameBytes, 30);
            localHeaders.push({ header: local, offset, crc, size: data.length });
            fileDatas.push(data);
            offset += local.length + data.length;
        }
        const centralParts = [];
        let centralSize = 0;
        for (let i = 0; i < files.length; i++) {
            const h = localHeaders[i];
            const nameBytes = new TextEncoder().encode(files[i].name);
            const central = new Uint8Array(46 + nameBytes.length);
            const v = new DataView(central.buffer);
            v.setUint32(0, 0x02014b50, true);
            v.setUint16(4, 20, true);
            v.setUint16(6, 20, true);
            v.setUint32(16, h.crc, true);
            v.setUint32(20, h.size, true);
            v.setUint32(24, h.size, true);
            v.setUint16(28, nameBytes.length, true);
            v.setUint32(42, h.offset, true);
            central.set(nameBytes, 46);
            centralParts.push(central);
            centralSize += central.length;
        }
        const end = new Uint8Array(22);
        const ev = new DataView(end.buffer);
        ev.setUint32(0, 0x06054b50, true);
        ev.setUint16(8, files.length, true);
        ev.setUint16(10, files.length, true);
        ev.setUint32(12, centralSize, true);
        ev.setUint32(16, offset, true);
        const total = offset + centralSize + 22;
        const zip = new Uint8Array(total);
        let pos = 0;
        for (let i = 0; i < files.length; i++) {
            zip.set(localHeaders[i].header, pos); pos += localHeaders[i].header.length;
            zip.set(fileDatas[i], pos); pos += fileDatas[i].length;
        }
        for (const c of centralParts) { zip.set(c, pos); pos += c.length; }
        zip.set(end, pos);
        return new Blob([zip], { type: 'application/zip' });
    }
    // ==================== Ambil Judul ====================
    function getTitle() {
    try {
        const h1 = document.querySelector('h1.text-xl') ||
                   document.querySelector('h1.font-black') ||
                   document.querySelector('h1');
        if (h1) {
            let text = h1.textContent.trim()
                .replace(/[\\/:*?"<>|]/g, '')
                .replace(/\s+/g, ' ')
                .trim();
            if (text.length > 3) return text.slice(0, 80);
        }
        let pageTitle = document.title || '';
        pageTitle = pageTitle.replace(/\s*[-|–]\s*DoujinDesu.*$/i, '').trim();
        if (pageTitle.length > 3) {
            return pageTitle.replace(/[\\/:*?"<>|]/g, '').slice(0, 80);
        }
    } catch (e) {}
    return '';
}
    // ==================== Deteksi ====================
    function isTargetImage(url) {
        if (!url || typeof url !== 'string') return false;
        const u = url.toLowerCase();
        const isImageExt = u.includes('.webp') || u.includes('.jpg') || u.includes('.jpeg') || u.includes('.png');
        if (!isImageExt) return false;
        return (
            u.includes('amz-ch.desu.pics') ||
            u.includes('desu.pics') ||
            u.includes('x-amz-') ||
            u.includes('storage/uploads') ||
            /\/[a-z0-9]{18,}\.(webp|jpg|jpeg|png)/i.test(u)
        );
    }
    function addUrl(url) {
        if (!url || imageURLs.includes(url)) return false;
        if (isTargetImage(url)) {
            imageURLs.push(url);
            return true;
        }
        return false;
    }
    function scanXAmz() {
        let changed = false;
        const newUrls = [];
        document.querySelectorAll('img').forEach(img => {
            const candidates = [img.src, img.currentSrc, img.dataset.src];
            for (const src of candidates) {
                if (src && isTargetImage(src) && !newUrls.includes(src) && !imageURLs.includes(src)) {
                    newUrls.push(src);
                }
            }
        });
        try {
            performance.getEntriesByType('resource').forEach(entry => {
                if (isTargetImage(entry.name) && !newUrls.includes(entry.name) && !imageURLs.includes(entry.name)) {
                    newUrls.push(entry.name);
                }
            });
        } catch (e) {}
        if (newUrls.length > 0) {
            imageURLs.push(...newUrls);
            changed = true;
        }
        if (changed) updatePanel();
        return changed;
    }
    if ('PerformanceObserver' in window) {
        try {
            new PerformanceObserver((list) => {
                let changed = false;
                for (const entry of list.getEntries()) {
                    if (addUrl(entry.name)) changed = true;
                }
                if (changed) updatePanel();
            }).observe({ type: 'resource', buffered: true });
        } catch (e) {}
    }
    // ==================== UI ====================
    function createButton() {
        if (document.getElementById('xamz-open-button')) return;
        const btn = document.createElement('button');
        btn.id = 'xamz-open-button';
        btn.innerHTML = `<img src="${ICON_FLOAT}" style="width:22px;height:22px;vertical-align:middle;margin-right:6px;border-radius:50%;"> DoujinDesu DL`;
        Object.assign(btn.style, {
            position: 'fixed', right: '15px', bottom: '15px', zIndex: '2147483647',
            background: '#ff2d95', color: '#000', border: 'none', borderRadius: '8px',
            padding: '8px 14px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer',
            boxShadow: '0 3px 15px rgba(255,45,149,.5)', display: 'flex', alignItems: 'center'
        });
        btn.onclick = openPanel;
        document.body.appendChild(btn);
    }
    function openPanel() {
        if (panelOpen) return;
        panelOpen = true;
        scanXAmz();
        autoScanInterval = setInterval(() => {
            if (panelOpen) scanXAmz();
        }, 2000);
        const autoTitle = getTitle();
        const overlay = document.createElement('div');
        overlay.id = 'xamz-overlay';
        overlay.innerHTML = `
            <div id="xamz-box">
                <div id="xamz-header">
                    <div><b>📚 DoujinDesu Downloader by qxyraxp</b> <span id="xamz-count">0 gambar</span></div>
                    <button id="xamz-close">✕</button>
                </div>
                <div id="xamz-toolbar">
                    <button id="xamz-refresh">🔄 Refresh</button>
                    <button id="xamz-select-all">☑ Semua</button>
                    <button id="xamz-select-none">☐ Batal</button>
                    <button id="xamz-download-selected"><img src="${ICON_SELECTED}" style="width:18px;height:18px;vertical-align:middle;margin-right:5px;border-radius:50%;"> Download Terpilih</button>
                    <button id="xamz-download-zip"><img src="${ICON_ZIP}" style="width:18px;height:18px;vertical-align:middle;margin-right:5px;border-radius:50%;"> Download ZIP</button>
                </div>
                <div id="xamz-zipname-row">
                    <label>Nama ZIP:</label>
                    <input id="xamz-zipname" type="text" value="${autoTitle}" placeholder="Nama file ZIP...">
                </div>
                <div id="xamz-status"></div>
                <div id="xamz-grid"></div>
            </div>
        `;
        document.body.appendChild(overlay);
        createStyles();
        document.getElementById('xamz-close').onclick = closePanel;
        document.getElementById('xamz-refresh').onclick = () => { scanXAmz(); updatePanel(); };
        document.getElementById('xamz-select-all').onclick = () => {
            document.querySelectorAll('#xamz-grid input[type="checkbox"]').forEach(cb => cb.checked = true);
        };
        document.getElementById('xamz-select-none').onclick = () => {
            document.querySelectorAll('#xamz-grid input[type="checkbox"]').forEach(cb => cb.checked = false);
        };
        document.getElementById('xamz-download-selected').onclick = downloadSelected;
        document.getElementById('xamz-download-zip').onclick = downloadAsZip;
        updatePanel();
    }
    function closePanel() {
        document.getElementById('xamz-overlay')?.remove();
        panelOpen = false;
        if (autoScanInterval) {
            clearInterval(autoScanInterval);
            autoScanInterval = null;
        }
    }
    function createStyles() {
        if (document.getElementById('xamz-style')) return;
        const style = document.createElement('style');
        style.id = 'xamz-style';
        style.textContent = `
            #xamz-overlay{position:fixed;inset:0;z-index:2147483646;background:rgba(0,0,0,.85);font-family:Arial,sans-serif;color:#fff}
            #xamz-box{position:absolute;left:3%;right:3%;top:3%;bottom:3%;background:#0a0a0a;border:2px solid #ff2d95;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 0 30px rgba(255,45,149,.3)}
            #xamz-header{min-height:55px;background:#1a0011;display:flex;align-items:center;padding:0 15px;border-bottom:1px solid #ff2d95}
            #xamz-count{margin-left:10px;color:#ff9ec8;font-size:13px}
            #xamz-close{margin-left:auto;background:#ff2d95;color:#000;border:none;border-radius:6px;padding:7px 13px;cursor:pointer;font-weight:bold}
            #xamz-close:hover{background:#ff5caf}
            #xamz-toolbar{background:#12000c;padding:9px;display:flex;flex-wrap:wrap;gap:7px;border-bottom:1px solid #ff2d95}
            #xamz-toolbar button{background:#ff2d95;color:#000;border:1px solid #ff2d95;border-radius:6px;padding:8px 12px;cursor:pointer;font-weight:bold;display:flex;align-items:center}
            #xamz-toolbar button:hover{background:#ff5caf;border-color:#ff5caf}
            #xamz-zipname-row{background:#0f000a;padding:8px 12px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #ff2d95}
            #xamz-zipname-row label{font-size:12px;color:#ff9ec8;white-space:nowrap}
            #xamz-zipname{flex:1;padding:7px 10px;border-radius:6px;border:1px solid #ff2d95;background:#1a0011;color:#fff;font-size:13px}
            #xamz-zipname:focus{outline:none;border-color:#ff5caf;box-shadow:0 0 0 2px rgba(255,45,149,.3)}
            #xamz-status{min-height:22px;padding:6px 12px;color:#ff9ec8;font-size:12px}
            #xamz-grid{flex:1;overflow:auto;padding:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;align-content:start;background:#0a0a0a}
            .xamz-item{position:relative;background:#12000c;border:1px solid #ff2d95;border-radius:7px;padding:7px}
            .xamz-item:hover{border-color:#ff5caf;box-shadow:0 0 10px rgba(255,45,149,.4)}
            .xamz-item img{width:100%;height:250px;object-fit:contain;display:block;background:#000;border-radius:4px;cursor:pointer}
            .xamz-item label{display:flex;align-items:center;gap:7px;margin-top:7px;font-size:12px;color:#ff9ec8}
            .xamz-number{position:absolute;left:12px;top:12px;background:rgba(255,45,149,.9);color:#000;padding:3px 7px;border-radius:4px;font-size:12px;font-weight:bold}
        `;
        document.head.appendChild(style);
    }
    function updatePanel() {
        const grid = document.getElementById('xamz-grid');
        if (!grid) return;
        document.getElementById('xamz-count').textContent = imageURLs.length + ' gambar';
        grid.innerHTML = '';
        if (!imageURLs.length) {
            grid.innerHTML = `<div style="grid-column:1/-1;text-align:center;padding:50px;color:#ff9ec8">
                Scroll halaman sampai gambar muncul, lalu tekan Refresh
            </div>`;
            return;
        }
        imageURLs.forEach((url, index) => {
            const item = document.createElement('div');
            item.className = 'xamz-item';
            item.innerHTML = `
                <div class="xamz-number">${String(index + 1).padStart(3, '0')}</div>
                <img src="${url}" loading="eager" data-index="${index}">
                <label><input type="checkbox" checked data-index="${index}"> Panel ${index + 1}</label>
            `;
            const img = item.querySelector('img');
            const checkbox = item.querySelector('input[type="checkbox"]');
            img.addEventListener('click', () => {
                checkbox.checked = !checkbox.checked;
            });
            grid.appendChild(item);
        });
    }
    function getSelectedIndexes() {
        return [...document.querySelectorAll('#xamz-grid input:checked')].map(cb => +cb.dataset.index);
    }
    function downloadSelected() {
        const indexes = getSelectedIndexes();
        if (!indexes.length) return alert('Pilih dulu panelnya');
        startDownloads(indexes);
    }
    // ==================== ZIP ====================
    async function downloadAsZip() {
        const indexes = getSelectedIndexes();
        if (!indexes.length) return alert('Pilih dulu panelnya');
        let zipName = (document.getElementById('xamz-zipname')?.value || '').trim()
            .replace(/[\\/:*?"<>|]/g, '');
        if (!zipName) zipName = 'DoujinDesu';
        if (!zipName.toLowerCase().endsWith('.zip')) zipName += '.zip';
        const status = document.getElementById('xamz-status');
        const files = [];
        let ok = 0, fail = 0;
        status.textContent = `⏳ Mengambil gambar 0/${indexes.length}`;
        for (let i = 0; i < indexes.length; i++) {
            const idx = indexes[i];
            status.textContent = `⏳ Mengambil gambar ${i + 1}/${indexes.length}`;
            try {
                const blob = await fetchImageAsBlob(imageURLs[idx]);
                if (blob) {
                    const ext = getExtFromUrl(imageURLs[idx]);
                    const name = String(idx + 1).padStart(3, '0') + '.' + ext;
                    files.push({ name, data: new Uint8Array(await blob.arrayBuffer()) });
                    ok++;
                } else fail++;
            } catch (e) {
                fail++;
            }
            await new Promise(r => setTimeout(r, 120));
        }
        if (ok === 0) {
            status.textContent = '❌ Tidak ada gambar yang berhasil diambil';
            return;
        }
        status.textContent = '⏳ Membuat ZIP...';
        try {
            const zipBlob = createZip(files);
            const a = document.createElement('a');
            a.href = URL.createObjectURL(zipBlob);
            a.download = zipName;
            a.click();
            URL.revokeObjectURL(a.href);
            status.textContent = `✅ ZIP berhasil! ${ok} gambar${fail > 0 ? `, ${fail} gagal` : ''}`;
        } catch (e) {
            status.textContent = '❌ Gagal membuat ZIP';
        }
    }
    // ==================== Helper ====================
    function getExtFromUrl(url) {
        try {
            const pathname = new URL(url).pathname.toLowerCase();
            if (pathname.endsWith('.webp')) return 'webp';
            if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) return 'jpg';
            if (pathname.endsWith('.png')) return 'png';
        } catch (e) {}
        return 'jpg';
    }
    function fetchImageAsBlob(url) {
        return new Promise((resolve) => {
            const img = [...document.querySelectorAll('#xamz-grid img')].find(i => i.src === url);
            if (img && img.complete && img.naturalWidth > 0) {
                try {
                    const canvas = document.createElement('canvas');
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    canvas.getContext('2d').drawImage(img, 0, 0);
                    const ext = getExtFromUrl(url);
                    const mime = ext === 'webp' ? 'image/webp' : 'image/jpeg';
                    canvas.toBlob(b => resolve(b), mime, 0.92);
                    return;
                } catch (e) {}
            }
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'arraybuffer',
                headers: {
                    'Referer': location.href,
                    'Origin': location.origin,
                    'Accept': 'image/webp,image/jpeg,image/*,*/*;q=0.8'
                },
                onload: (res) => {
                    if (res.status >= 200 && res.status < 300) {
                        let mime = 'image/jpeg';
                        const ct = (res.responseHeaders || '').toLowerCase();
                        if (ct.includes('image/webp')) mime = 'image/webp';
                        else if (ct.includes('image/png')) mime = 'image/png';
                        resolve(new Blob([res.response], { type: mime }));
                    } else resolve(null);
                },
                onerror: () => resolve(null),
                ontimeout: () => resolve(null)
            });
        });
    }
    async function downloadOne(index) {
        const url = imageURLs[index];
        const ext = getExtFromUrl(url);
        const filename = String(index + 1).padStart(3, '0') + '.' + ext;
        try {
            const blob = await fetchImageAsBlob(url);
            if (blob) {
                const a = document.createElement('a');
                a.href = URL.createObjectURL(blob);
                a.download = filename;
                a.click();
                URL.revokeObjectURL(a.href);
                return true;
            }
        } catch (e) {}
        try {
            await new Promise((resolve, reject) => {
                GM_download({ url, name: filename, saveAs: false, onload: resolve, onerror: reject });
            });
            return true;
        } catch (e) {}
        return false;
    }
    function startDownloads(indexes) {
        const status = document.getElementById('xamz-status');
        let ok = 0, fail = 0;
        status.textContent = `⏳ 0/${indexes.length}`;
        (async () => {
            for (let i = 0; i < indexes.length; i++) {
                status.textContent = `⏳ ${i + 1}/${indexes.length}`;
                if (await downloadOne(indexes[i])) ok++;
                else fail++;
                await new Promise(r => setTimeout(r, 300));
            }
            status.textContent = `✅ Berhasil ${ok}, gagal ${fail}`;
        })();
    }
    function init() {
        if (!document.body) return setTimeout(init, 300);
        createButton();
        setTimeout(scanXAmz, 1500);
        setTimeout(scanXAmz, 4000);
    }
    init();
})();