Greasy Fork is available in English.

LayoutDownloaderAtt

Script Universal de CSS pro Ultimate Scrapper

Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/585205/1865576/LayoutDownloaderAtt.js

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

/*
 * Dependências:
 *
 * GM_info(optional)
 * Docs: https://violentmonkey.github.io/api/gm/#gm_info
 * 
 * GM_xmlhttpRequest(optional)
 * Docs: https://violentmonkey.github.io/api/gm/#gm_xmlhttprequest
 *
 * JSZIP
 * Github: https://github.com/Stuk/jszip
 * CDN: https://unpkg.com/[email protected]/dist/jszip.min.js
 *
 * FileSaver
 * Github: https://github.com/eligrey/FileSaver.js
 * CDN: https://unpkg.com/[email protected]/dist/FileSaver.min.js
 */

;
const LayoutDownloaderAtt = (({ JSZip, saveAs }) => {
    let maxNum = 0;
    let promiseCount = 0;
    let fulfillCount = 0;
    let isErrorOccurred = false;

    // Elementos
    let startNumInputElement = null;
    let endNumInputElement = null;
    let downloadButtonElement = null;
    let statusElement = null;
    let panelElement = null;
    let guiHost = null;
    let qualitySliderElement = null;
    let qualityValElement = null;

    // Estado
    let selectedExt = 'jpg';

    // svg icons
    const externalLinkSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16" style="margin-left: 5px;"><path fill-rule="evenodd" d="M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z"></path></svg>`;
    const reloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentcolor" width="16" height="16" style="margin-left: 5px;"><path fill-rule="evenodd" d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"></path></svg>`;
    
    // Inicialização
    function init({
        maxImageAmount,
        getImagePromises,
        title = `package_${Date.now()}`,
        imageSuffix = 'jpg',
        zipOptions = {},
        positionOptions = {}
    }) {
        maxNum = maxImageAmount;
        selectedExt = imageSuffix;

        setupUI(positionOptions);
        setupUpdateNotification();

        if (downloadButtonElement) {
            downloadButtonElement.onclick = function () {
                if (!isOKToDownload()) return;
                this.disabled = true;
                this.textContent = "Processing...";
                this.style.backgroundColor = '#9ca3af';
                this.style.cursor = 'not-allowed';
                this.style.boxShadow = 'none';
                statusElement.innerHTML = "<span style='color:#3b82f6; font-weight:bold;'>Starting download...</span>";

                const qualityValue = Number(qualitySliderElement.value);
                download(getImagePromises, title, selectedExt, zipOptions, qualityValue);
            }
        }
    }

    function setupUI(positionOptions) {
        if (document.getElementById('cd-gui-container')) return;
        guiHost = document.createElement('div');
        guiHost.id = 'cd-gui-container';
        document.body.appendChild(guiHost);

        const shadow = guiHost.attachShadow({ mode: 'open' });

        const style = document.createElement('style');
        style.innerHTML = `
        :host { all: initial; }
        *, *::before, *::after { box-sizing: border-box; }
        #cd-panel { position: fixed; top: 20px; right: 20px; width: 340px; background: #ffffff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; color: #333; box-sizing: border-box; user-select: none; }
        .cd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
        .cd-title { font-weight: 700; color: #3b82f6; font-size: 16px; display: flex; align-items: center; gap: 8px; }
        .cd-close { border: none; background: none; font-size: 20px; color: #9ca3af; cursor: pointer; padding: 0; line-height: 1; transition: color 0.2s; font-weight: bold; }
        .cd-close:hover { color: #ef4444; }
        .cd-status { font-size: 14px; margin-bottom: 16px; color: #10b981; font-weight:bold; min-height: 40px; text-align: center; display: flex; align-items: center; justify-content: center; }
        .cd-box { border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px; margin-bottom: 20px; }
        .cd-box-title { font-size: 11px; font-weight: 700; color: #3b82f6; text-transform: uppercase; text-align: center; margin-bottom: 12px; letter-spacing: 0.5px; }
        .cd-res-inputs { display: flex; justify-content: center; align-items: center; gap: 12px; }
        .cd-res-input { width: 80px; padding: 8px; border: 1px solid #e2e8f0; border-radius: 6px; background: #f8fafc; text-align: center; color: #64748b; font-weight: 600; font-size: 14px; outline: none; }
        .cd-res-input:focus { border-color: #3b82f6; }
        .cd-format-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
        .cd-fmt-btn { border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 8px; background: #fff; cursor: pointer; text-align: center; transition: all 0.2s; color: #1f2937; font-weight: 600; font-size: 14px; display: flex; flex-direction: column; align-items: center; gap: 4px; }
        .cd-fmt-btn span { font-size: 11px; font-weight: 400; color: #6b7280; }
        .cd-fmt-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); }
        .cd-fmt-btn.active span { color: #dbeafe; }
        .cd-fmt-btn:disabled { opacity: 0.5; cursor: not-allowed; background: #f9fafb; }
        .cd-slider-container { margin-bottom: 20px; }
        .cd-slider-label { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px; display: flex; justify-content: space-between; }
        .cd-slider { width: 100%; cursor: pointer; }
        .cd-main-btn { width: 100%; padding: 14px; border: none; border-radius: 8px; background: #2563eb; color: #fff; font-size: 15px; font-weight: 700; cursor: pointer; transition: background 0.2s; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); margin-bottom: 16px; }
        .cd-main-btn:hover { background: #1d4ed8; }
        .cd-version { text-align: center; font-size: 11px; color: #9ca3af; margin-top: 16px; }
        .cd-warning { background: #fef9c3; border: 1px solid #fde047; border-radius: 8px; padding: 12px; color: #854d0e; font-size: 12px; text-align: center; line-height: 1.5; font-weight: 500; }
    `;
        shadow.appendChild(style);

        panelElement = document.createElement('div');
        panelElement.id = 'cd-panel';
        for (const [key, value] of Object.entries(positionOptions)) {
            if (key === 'top' || key === 'bottom' || key === 'left' || key === 'right') {
                panelElement.style[key] = value;
            }
        }

        panelElement.innerHTML = `
        <div class="cd-header">
            <div class="cd-title">🔥 Ultimate Scrapper</div>
            <button id="cd-close" class="cd-close">&times;</button>
        </div>
        <div id="cd-status" class="cd-status">Ready to download!</div>
        <div class="cd-box">
            <div class="cd-box-title">Page Range</div>
            <div class="cd-res-inputs">
                <input type="number" id="cd-start-num" class="cd-res-input" value="1" min="1" max="${maxNum}">
                <span style="color: #cbd5e1; font-size: 14px; font-weight: bold;">until</span>
                <input type="number" id="cd-end-num" class="cd-res-input" value="${maxNum}" min="1" max="${maxNum}">
            </div>
        </div>
        
        <div style="font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 12px;">Image's Output</div>
        <div class="cd-format-grid">
            <button class="cd-fmt-btn ${selectedExt === 'webp' ? 'active' : ''}" data-ext="webp">WebP</button>
            <button class="cd-fmt-btn ${selectedExt === 'jpg' ? 'active' : ''}" data-ext="jpg">JPEG</button>
            <button class="cd-fmt-btn ${selectedExt === 'png' ? 'active' : ''}" data-ext="png">PNG</button>
            <button class="cd-fmt-btn ${selectedExt === 'jxl' ? 'active' : ''}" data-ext="jxl">JXL</button>
        </div>

        <div class="cd-slider-container">
            <div class="cd-slider-label">
                <span>Quality:</span>
                <span style="color: #3b82f6;"><span id="cd-quality-val">100</span>%</span>
            </div>
            <input type="range" id="cd-quality" min="0" max="100" value="100" class="cd-slider">
        </div>

        <button id="cd-start" class="cd-main-btn">Download</button>
        <div class="cd-warning" style="margin-top:10px;">⚠️ When you download a chapter and, if by chance, you want to download another one right after, you <b>NEED</b> to reload the page. Otherwise, it'll download the manga you downloaded last.</div>
        <div class="cd-version">Powered By: Nkkz</div>
        `;
        shadow.appendChild(panelElement);

        startNumInputElement = shadow.getElementById('cd-start-num');
        endNumInputElement = shadow.getElementById('cd-end-num');
        downloadButtonElement = shadow.getElementById('cd-start');
        statusElement = shadow.getElementById('cd-status');
        
        qualitySliderElement = shadow.getElementById('cd-quality');
        qualityValElement = shadow.getElementById('cd-quality-val');

        startNumInputElement.onkeydown = (e) => e.stopPropagation();
        endNumInputElement.onkeydown = (e) => e.stopPropagation();

        endNumInputElement.addEventListener('input', (e) => {
            if (parseInt(e.target.value, 10) > maxNum) {
                e.target.value = maxNum;
            }
        });

        startNumInputElement.addEventListener('input', (e) => {
            if (parseInt(e.target.value, 10) > maxNum) {
                e.target.value = maxNum;
            }
        });

        qualitySliderElement.onkeydown = (e) => e.stopPropagation();

        qualitySliderElement.oninput = (e) => {
            qualityValElement.textContent = e.target.value;
        };

        shadow.getElementById('cd-close').onclick = () => { guiHost.remove(); };

        function updateSliderState(ext) {
            if (ext === 'jxl' || ext === 'png') {
                qualitySliderElement.disabled = true;
                qualitySliderElement.style.opacity = '0.4';
                qualitySliderElement.style.cursor = 'not-allowed';
                qualityValElement.style.opacity = "0.4";
            } else {
                qualitySliderElement.disabled = false;
                qualitySliderElement.style.opacity = '1';
                qualitySliderElement.style.cursor = 'pointer';
                qualityValElement.style.opacity = "1";
            }
        }
        updateSliderState(selectedExt);

        shadow.querySelectorAll('.cd-fmt-btn').forEach(btn => {
            btn.onclick = () => {
                shadow.querySelectorAll('.cd-fmt-btn').forEach(b => b.classList.remove('active'));
                btn.classList.add('active');
               
                selectedExt = btn.getAttribute('data-ext');
                updateSliderState(selectedExt);
            };
        });
    }

    async function setupUpdateNotification() {
        if (typeof GM_info === 'undefined' || typeof GM_xmlhttpRequest === 'undefined') return;
        const localVersion = Number(GM_info.script.version);

        const scriptID = (GM_info.script.homepageURL || GM_info.script.homepage).match(/scripts\/(?<id>\d+)-/)?.groups?.id;
        const scriptURL = `https://update.greasyfork.org/scripts/${scriptID}/raw.js`;
        const latestVersionString = await new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: scriptURL,
                responseType: 'text',
                onload: res => resolve(res.response.match(/@version\s+(?<version>[0-9\.]+)/)?.groups?.version)
            });
        });

        const latestVersion = Number(latestVersionString);

        if (Number.isNaN(localVersion) || Number.isNaN(latestVersion)) return;
        if (latestVersion <= localVersion) return;

        const updateLinkElement = document.createElement('a');
        updateLinkElement.id = 'LayoutDownloaderAtt-UpdateLink';
        updateLinkElement.href = scriptURL.replace('raw.js', 'raw.user.js');
        updateLinkElement.innerHTML = `Update v${latestVersionString} is out! ${externalLinkSVG}`;
        updateLinkElement.style = `
      display: flex; justify-content: center; align-items: center;
      margin-top: 16px; padding: 12px; font-size: 13px; font-weight: bold;
      text-decoration: none; color: white; background-color: #10b981;
      border-radius: 8px; cursor: pointer; transition: background 0.2s;
    `;

        updateLinkElement.onmouseover = () => updateLinkElement.style.backgroundColor = '#059669';
        updateLinkElement.onmouseout = () => updateLinkElement.style.backgroundColor = '#10b981';
        updateLinkElement.onclick = () => setTimeout(() => {
            updateLinkElement.removeAttribute('href');
            updateLinkElement.innerHTML = `Please, reload the guide. ${reloadSVG}`;
            updateLinkElement.style.cursor = 'default';
            updateLinkElement.style.backgroundColor = '#f59e0b';
        }, 1000);
        panelElement.appendChild(updateLinkElement);
    }

    function isOKToDownload() {
        const startNum = Number(startNumInputElement.value);
        const endNum = Number(endNumInputElement.value);

        if (Number.isNaN(startNum) || Number.isNaN(endNum)) { alert("Please enter page number correctly."); return false; }
        if (!Number.isInteger(startNum) || !Number.isInteger(endNum)) { alert("Please enter integers correctly."); return false; }
        if (startNum < 1 || endNum < 1) { alert("Page number shouldn't smaller than 1."); return false; }
        if (startNum > maxNum || endNum > maxNum) { alert(`Page number shouldn't bigger than ${maxNum}.`); return false; }
        if (startNum > endNum) { alert("Number of start shouldn't bigger than number of end."); return false; }

        return true;
    }

    async function download(getImagePromises, title, imageSuffix, zipOptions, qualityValue) {
        const startNum = Number(startNumInputElement.value);
        const endNum = Number(endNumInputElement.value);
        promiseCount = endNum - startNum + 1;
        fulfillCount = 0;
        // reseta
        isErrorOccurred = false;

        let images = [];
        for (let num = startNum; num <= endNum; num += 4) {
            const from = num;
            const to = Math.min(num + 3, endNum);
            try {
                const result = await Promise.all(getImagePromises(from, to, imageSuffix, qualityValue));
                images = images.concat(result);
            } catch (error) {
                return;
            }
        }

        JSZip.defaults.date = new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000);
        const zip = new JSZip();
        const zipTitle = title.replaceAll(/\/|\\|\:|\*|\?|\"|\<|\>|\|/g, '');
        const folder = zip.folder(zipTitle);

        for (const [index, image] of images.entries()) {
            const filename = `${String(startNum + index).padStart(maxNum >= 100 ? String(maxNum).length : 2, '0')}.${imageSuffix}`;
            folder.file(filename, image, zipOptions);
        }

        const zipProgressHandler = (metadata) => {
            downloadButtonElement.textContent = `Zipping...`;
            statusElement.innerHTML = `Compacting ZIP...ᅠ<b style="color:#3b82f6;">ᅠ${metadata.percent.toFixed()}%</b>`;
        }
        const content = await zip.generateAsync({ type: "blob" }, zipProgressHandler);

        saveAs(content, `${zipTitle}.zip`);

        downloadButtonElement.textContent = "Download Complete. (Download again?)";
        downloadButtonElement.style.backgroundColor = '#10b981';
        downloadButtonElement.style.cursor = 'pointer'; 
        downloadButtonElement.disabled = false; 
        statusElement.innerHTML = `<span style="color:#10b981; font-weight:bold;">Sucess! Enjoy it!</span>`;
    }

    function fulfillHandler(res) {
        if (!isErrorOccurred) {
            fulfillCount++;
            downloadButtonElement.textContent = `Processing...`;
            statusElement.innerHTML = `Downloading the page:ᅠ<b>${fulfillCount}/${promiseCount}</b>ᅠ(${Math.round((fulfillCount / promiseCount) * 100)}%)`;
        }
        return res;
    }

    function rejectHandler(err) {
        isErrorOccurred = true;
        console.error(err);

        downloadButtonElement.textContent = 'Error Occurred(Try again?)';
        downloadButtonElement.style.backgroundColor = '#ef4444';
        downloadButtonElement.style.cursor = 'pointer';
        downloadButtonElement.disabled = false; 
        statusElement.innerHTML = `<span style="color:#ef4444; font-weight:bold;">Error to download the page.</span>`;

        return Promise.reject(err);
    }

    return { init, fulfillHandler, rejectHandler };
})(window);