LayoutDownloaderAtt

Script Universal de CSS pro Ultimate Scrapper

Este script não deve ser instalado diretamente. É uma biblioteca destinada a ser incluída por outros scripts através da diretiva de metadados // @require https://update.greasyfork.org/scripts/585205/1865576/LayoutDownloaderAtt.js

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

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