LayoutDownloaderAtt

Script Universal de CSS pro Ultimate Scrapper

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/585205/1865576/LayoutDownloaderAtt.js

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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);