LayoutDownloaderAtt

Script Universal de CSS pro Ultimate Scrapper

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greasyfork.org/scripts/585205/1865576/LayoutDownloaderAtt.js

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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