[E/Ex-Hentai] Downloader

漫畫頁面創建下載按鈕, 可切換 (壓縮下載 | 單圖下載), 無須複雜設置一鍵點擊下載, 自動獲取(非原圖)進行下載

// ==UserScript==
// @name         [E/Ex-Hentai] Downloader
// @name:zh-TW   [E/Ex-Hentai] 下載器
// @name:zh-CN   [E/Ex-Hentai] 下载器
// @name:ja      [E/Ex-Hentai] ダウンローダー
// @name:ko      [E/Ex-Hentai] 다운로더
// @name:ru      [E/Ex-Hentai] Загрузчик
// @name:en      [E/Ex-Hentai] Downloader
// @version      2025.09.05-Beta
// @author       Canaan HS
// @description         漫畫頁面創建下載按鈕, 可切換 (壓縮下載 | 單圖下載), 無須複雜設置一鍵點擊下載, 自動獲取(非原圖)進行下載
// @description:zh-TW   漫畫頁面創建下載按鈕, 可切換 (壓縮下載 | 單圖下載), 無須複雜設置一鍵點擊下載, 自動獲取(非原圖)進行下載
// @description:zh-CN   漫画页面创建下载按钮, 可切换 (压缩下载 | 单图下载), 无须复杂设置一键点击下载, 自动获取(非原图)进行下载
// @description:ja      マンガページにダウンロードボタンを作成し、(圧縮ダウンロード | シングルイメージダウンロード)を切り替えることができ、複雑な設定は必要なく、ワンクリックでダウンロードできます。自動的に(オリジナルではない)画像を取得してダウンロードします
// @description:ko      만화 페이지에 다운로드 버튼을 만들어 (압축 다운로드 | 단일 이미지 다운로드)를 전환할 수 있으며, 복잡한 설정이 필요하지 않고, 원클릭 다운로드 기능으로 (원본이 아닌) 이미지를 자동으로 가져와 다운로드합니다
// @description:ru      Создание кнопок загрузки на страницах манги, переключение между (сжатой загрузкой | загрузкой отдельных изображений), без необходимости сложных настроек, возможность загрузки одним кликом, автоматически получает (неоригинальные) изображения для загрузки
// @description:en      Create download buttons on manga pages, switchable between (compressed download | single image download), without the need for complex settings, one-click download capability, automatically fetches (non-original) images for downloading

// @connect      *
// @match        *://e-hentai.org/g/*
// @match        *://exhentai.org/g/*
// @icon         https://e-hentai.org/favicon.ico

// @license      MPL-2.0
// @namespace    https://greasyfork.org/users/989635
// @supportURL   https://github.com/Canaan-HS/MonkeyScript/issues

// @require      https://update.greasyfork.org/scripts/495339/1654307/Syntax_min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js

// @grant        window.close
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @grant        GM_addElement
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand

// @run-at       document-body
// ==/UserScript==

(function () {
    const Config = {
        Dev: true,            // 開發模式 (會顯示除錯訊息)
        ReTry: 10,            // 下載錯誤重試次數, 超過這個次數該圖片會被跳過
        Original: false,      // 是否下載原圖
        ResetScope: true,     // 下載完成後 重置範圍設置
        CompleteClose: false, // 下載完成自動關閉
    };
    const DConfig = {
        Compress_Level: 9,
        MIN_CONCURRENCY: 5,
        MAX_CONCURRENCY: 16,
        MAX_Delay: 2500,
        Home_ID: 100,
        Home_ND: 80,
        Image_ID: 34,
        Image_ND: 28,
        Download_IT: 6,
        Download_ID: 600,
        Download_ND: 300,
        Lock: false,
        SortReverse: false,
        Scope: void 0,
        TitleCache: void 0,
        ModeDisplay: void 0,
        CompressMode: void 0,
        KeyCache: void 0,
        GetKey: function () {
            if (!this.KeyCache) this.KeyCache = `DownloadCache_${location.pathname.split("/").slice(2, 4).join("")}`;
            return this.KeyCache;
        }
    };
    const Dict = {
        Traditional: {
            "範圍設置": "下載完成後自動重置\n\n單項設置: 1. 2, 3\n範圍設置: 1~5, 6-10\n排除設置: !5, -10\n"
        },
        Simplified: {
            "🚮 清除數據緩存": "🚮 清除数据缓存",
            "🔁 切換下載模式": "🔁 切换下载模式",
            "⚙️ 下載範圍設置": "⚙️ 下载范围设置",
            "📥 強制壓縮下載": "📥 强制压缩下载",
            "⛔️ 終止下載": "⛔️ 取消下载",
            "壓縮下載": "压缩下载",
            "單圖下載": "单图下载",
            "下載中鎖定": "下载中锁定",
            "開始下載": "开始下载",
            "獲取頁面": "获取页面中",
            "獲取連結": "获取链接中",
            "下載進度": "下载进度",
            "壓縮進度": "压缩进度",
            "壓縮完成": "压缩完成",
            "壓縮失敗": "压缩失败",
            "下載完成": "下载完成",
            "清理警告": "清理提示",
            "任務配置": "任务配置",
            "取得結果": "获取结果",
            "重新取得數據": "重新获取数据",
            "確認設置範圍": "确认设置范围",
            "剩餘重載次數": "剩余重试次数",
            "下載失敗數據": "下载失败数据",
            "內頁跳轉數據": "内页跳转数据",
            "圖片連結數據": "图片链接数据",
            "等待失敗重試...": "等待失败重试...",
            "請求錯誤重新加載頁面": "请求错误,请刷新页面",
            "檢測到圖片集 !!\n\n是否反轉排序後下載 ?": "检测到图片集!\n\n是否按反向顺序下载?",
            "下載數據不完整將清除緩存, 建議刷新頁面後重載": "下载数据不完整,将清除缓存。建议刷新页面后重试",
            "找不到圖片元素, 你的 IP 可能被禁止了, 請刷新頁面重試": "找不到图片元素,您的 IP 可能被禁止。请刷新页面重试",
            "範圍設置": "下载完成后自动重置\n\n单项设置:1, 2, 3\n范围设置:1~5, 6-10\n排除设置:!5, -10\n"
        },
        Japan: {
            "🚮 清除數據緩存": "🚮 データキャッシュを削除",
            "🔁 切換下載模式": "🔁 ダウンロードモードの切り替え",
            "⚙️ 下載範圍設置": "⚙️ ダウンロード範囲設定",
            "📥 強制壓縮下載": "📥 強制圧縮ダウンロード",
            "⛔️ 終止下載": "⛔️ ダウンロードを中止",
            "壓縮下載": "圧縮ダウンロード",
            "單圖下載": "単一画像ダウンロード",
            "下載中鎖定": "ダウンロード中ロック",
            "開始下載": "ダウンロード開始",
            "獲取頁面": "ページ取得中",
            "獲取連結": "リンク取得中",
            "下載進度": "ダウンロード進捗",
            "壓縮進度": "圧縮進捗",
            "壓縮完成": "圧縮完了",
            "壓縮失敗": "圧縮失敗",
            "下載完成": "ダウンロード完了",
            "清理警告": "クリーンアップ警告",
            "任務配置": "タスク設定",
            "取得結果": "結果を取得",
            "重新取得數據": "データを再取得",
            "確認設置範圍": "範囲設定を確認",
            "剩餘重載次數": "残りの再試行回数",
            "下載失敗數據": "ダウンロード失敗データ",
            "內頁跳轉數據": "内部ページリダイレクトデータ",
            "圖片連結數據": "画像リンクデータ",
            "等待失敗重試...": "失敗の再試行を待機中...",
            "請求錯誤重新加載頁面": "リクエストエラー。ページを再読み込みしてください",
            "檢測到圖片集 !!\n\n是否反轉排序後下載 ?": "画像集が検出されました!\n\n逆順でダウンロードしますか?",
            "下載數據不完整將清除緩存, 建議刷新頁面後重載": "ダウンロードデータが不完全です。キャッシュがクリアされます。ページを更新して再度お試しください",
            "找不到圖片元素, 你的 IP 可能被禁止了, 請刷新頁面重試": "画像要素が見つかりません。IPがブロックされている可能性があります。ページを更新して再試行してください",
            "範圍設置": "ダウンロード完了後に自動リセット\n\n単一項目: 1, 2, 3\n範囲指定: 15, 6-10\n除外設定: !5, -10\n"
        },
        Korea: {
            "🚮 清除數據緩存": "🚮 데이터 캐시 삭제",
            "🔁 切換下載模式": "🔁 다운로드 모드 전환",
            "⚙️ 下載範圍設置": "⚙️ 다운로드 범위 설정",
            "📥 強制壓縮下載": "📥 강제 압축 다운로드",
            "⛔️ 終止下載": "⛔️ 다운로드 중단",
            "壓縮下載": "압축 다운로드",
            "單圖下載": "단일 이미지 다운로드",
            "下載中鎖定": "다운로드 중 잠금",
            "開始下載": "다운로드 시작",
            "獲取頁面": "페이지 가져오는 중",
            "獲取連結": "링크 가져오는 중",
            "下載進度": "다운로드 진행률",
            "壓縮進度": "압축 진행률",
            "壓縮完成": "압축 완료",
            "壓縮失敗": "압축 실패",
            "下載完成": "다운로드 완료",
            "清理警告": "정리 경고",
            "任務配置": "작업 구성",
            "取得結果": "결과 가져오기",
            "重新取得數據": "데이터 새로고침",
            "確認設置範圍": "범위 설정 확인",
            "剩餘重載次數": "남은 재시도 횟수",
            "下載失敗數據": "다운로드 실패 데이터",
            "內頁跳轉數據": "내부 페이지 이동 데이터",
            "圖片連結數據": "이미지 링크 데이터",
            "等待失敗重試...": "실패 후 재시도 대기 중...",
            "請求錯誤重新加載頁面": "요청 오류. 페이지를 다시 로드하세요",
            "檢測到圖片集 !!\n\n是否反轉排序後下載 ?": "이미지 모음이 감지되었습니다!\n\n역순으로 다운로드하시겠습니까?",
            "下載數據不完整將清除緩存, 建議刷新頁面後重載": "다운로드 데이터가 불완전합니다. 캐시가 지워집니다. 페이지를 새로고침하고 다시 시도하세요",
            "找不到圖片元素, 你的 IP 可能被禁止了, 請刷新頁面重試": "이미지 요소를 찾을 수 없습니다. IP가 차단되었을 수 있습니다. 페이지를 새로고침하고 다시 시도하세요",
            "範圍設置": "다운로드 완료 후 자동 재설정\n\n단일 항목: 1, 2, 3\n범위 지정: 15, 6-10\n제외 설정: !5, -10\n"
        },
        Russia: {
            "🚮 清除數據緩存": "🚮 Очистить кэш данных",
            "🔁 切換下載模式": "🔁 Переключить режим загрузки",
            "⚙️ 下載範圍設置": "⚙️ Настройки диапазона загрузки",
            "📥 強制壓縮下載": "📥 Принудительная сжатая загрузка",
            "⛔️ 終止下載": "⛔️ Прервать загрузку",
            "壓縮下載": "Сжатая загрузка",
            "單圖下載": "Загрузка отдельных изображений",
            "下載中鎖定": "Заблокировано во время загрузки",
            "開始下載": "Начать загрузку",
            "獲取頁面": "Получить страницу",
            "獲取連結": "Получить ссылку",
            "下載進度": "Прогресс загрузки",
            "壓縮進度": "Прогресс сжатия",
            "壓縮完成": "Сжатие завершено",
            "壓縮失敗": "Ошибка сжатия",
            "下載完成": "Загрузка завершена",
            "清理警告": "Предупреждение об очистке",
            "任務配置": "Конфигурация задачи",
            "取得結果": "Получить результаты",
            "重新取得數據": "Повторно получить данные",
            "確認設置範圍": "Подтвердить настройки диапазона",
            "剩餘重載次數": "Оставшиеся попытки перезагрузки",
            "下載失敗數據": "Данные о неудачных загрузках",
            "內頁跳轉數據": "Данные о перенаправлении внутренней страницы",
            "圖片連結數據": "Данные о ссылках на изображения",
            "等待失敗重試...": "Ожидание повторной попытки после сбоя...",
            "請求錯誤重新加載頁面": "Ошибка запроса, перезагрузите страницу",
            "檢測到圖片集 !!\n\n是否反轉排序後下載 ?": "Обнаружена коллекция изображений !!\n\nХотите изменить порядок сортировки перед загрузкой?",
            "下載數據不完整將清除緩存, 建議刷新頁面後重載": "Данные загрузки неполные, кэш будет очищен, рекомендуется обновить страницу и перезагрузить",
            "找不到圖片元素, 你的 IP 可能被禁止了, 請刷新頁面重試": "Элементы изображения не найдены, возможно, ваш IP заблокирован, пожалуйста, обновите страницу и попробуйте снова",
            "範圍設置": "Автоматический сброс после завершения загрузки\n\nНастройки отдельных элементов: 1. 2, 3\nНастройки диапазона: 1~5, 6-10\nНастройки исключения: !5, -10\n"
        },
        English: {
            "🚮 清除數據緩存": "🚮 Clear Data Cache",
            "🔁 切換下載模式": "🔁 Switch Download Mode",
            "⚙️ 下載範圍設置": "⚙️ Download Range Settings",
            "📥 強制壓縮下載": "📥 Force Compressed Download",
            "⛔️ 終止下載": "⛔️ Cancel Download",
            "壓縮下載": "Compressed Download",
            "單圖下載": "Single Image Download",
            "下載中鎖定": "Locked During Download",
            "開始下載": "Start Download",
            "獲取頁面": "Fetching Page",
            "獲取連結": "Fetching Links",
            "下載進度": "Download Progress",
            "壓縮進度": "Compression Progress",
            "壓縮完成": "Compression Complete",
            "壓縮失敗": "Compression Failed",
            "下載完成": "Download Complete",
            "清理警告": "Cleanup Warning",
            "任務配置": "Task Configuration",
            "取得結果": "Get Results",
            "重新取得數據": "Refresh Data",
            "確認設置範圍": "Confirm Range Settings",
            "剩餘重載次數": "Remaining Retry Attempts",
            "下載失敗數據": "Failed Download Data",
            "內頁跳轉數據": "Internal Page Navigation Data",
            "圖片連結數據": "Image Link Data",
            "等待失敗重試...": "Waiting for failed retry...",
            "請求錯誤重新加載頁面": "Request error. Please reload the page.",
            "檢測到圖片集 !!\n\n是否反轉排序後下載 ?": "Image collection detected!\n\nDo you want to download in reverse order?",
            "下載數據不完整將清除緩存, 建議刷新頁面後重載": "Incomplete download data. Cache will be cleared. We recommend refreshing the page and trying again.",
            "找不到圖片元素, 你的 IP 可能被禁止了, 請刷新頁面重試": "Image elements not found. Your IP may be blocked. Please refresh the page and try again.",
            "範圍設置": "Settings automatically reset after download completes.\n\nSingle items: 1, 2, 3\nRanges: 1~5, 6-10\nExclusions: !5, -10\n"
        }
    };
    function Downloader(GM_xmlhttpRequest2, GM_download2, Config2, DConfig2, Transl, Lib2, saveAs2) {
        const zipper = Lib2.createCompressor();
        const dynamicParam = Lib2.createNnetworkObserver({
            MAX_Delay: DConfig2.MAX_Delay,
            MIN_CONCURRENCY: DConfig2.MIN_CONCURRENCY,
            MAX_CONCURRENCY: DConfig2.MAX_CONCURRENCY,
            Good_Network_THRESHOLD: 500,
            Poor_Network_THRESHOLD: 1500
        });
        const getTotal = page => Math.ceil(+page[page.length - 2].$text().replace(/\D/g, "") / 20);
        return (url, button) => {
            let comicName = null;
            const worker = Lib2.workerCreate(`
            let queue = [], processing = false;
            onmessage = function(e) {
                queue.push(e.data);
                !processing && (processing = true, processQueue());
            }
            async function processQueue() {
                if (queue.length > 0) {
                    const {index, url, time, delay} = queue.shift();
                    FetchRequest(index, url, time, delay);
                    setTimeout(processQueue, delay);
                } else {processing = false}
            }
            async function FetchRequest(index, url, time, delay) {
                try {
                    const response = await fetch(url);
                    const html = await response.text();
                    postMessage({index, url, html, time, delay, error: false});
                } catch {
                    postMessage({index, url, html: null, time, delay, error: true});
                }
            }
        `);
            getHomeData();
            async function reset() {
                Config2.CompleteClose && window.close();
                Config2.ResetScope && (DConfig2.Scope = false);
                worker.terminate();
                button = Lib2.$q("#ExDB");
                button.disabled = false;
                button.$text(`✓ ${DConfig2.ModeDisplay}`);
                DConfig2.Lock = false;
            }
            async function getHomeData() {
                comicName = Lib2.nameFilter(Lib2.$q("#gj").$text() || Lib2.$q("#gn").$text());
                const ct6 = Lib2.$q("#gdc .ct6");
                const cacheData = Lib2.session(DConfig2.GetKey());
                if (ct6) {
                    const yes = confirm(Transl("檢測到圖片集 !!\n\n是否反轉排序後下載 ?"));
                    DConfig2.SortReverse = yes ? true : false;
                }
                if (cacheData) {
                    startTask(cacheData);
                    return;
                }
                const pages = getTotal(Lib2.$qa("#gdd td.gdt2"));
                worker.onmessage = e => {
                    const {
                        index,
                        url: url2,
                        html,
                        time,
                        delay: delay2,
                        error
                    } = e.data;
                    error ? worker.postMessage({
                        index: index,
                        url: url2,
                        time: time,
                        delay: dynamicParam(time, delay2, null, DConfig2.Home_ND)
                    }) : parseLink(index, Lib2.domParse(html));
                };
                const delay = DConfig2.Home_ID;
                worker.postMessage({
                    index: 0,
                    url: url,
                    time: Date.now(),
                    delay: delay
                });
                for (let index = 1; index < pages; index++) {
                    worker.postMessage({
                        index: index,
                        url: `${url}?p=${index}`,
                        time: Date.now(),
                        delay: delay
                    });
                }
                let task = 0;
                let processed = new Set();
                const homeData = new Map();
                function parseLink(index, page) {
                    try {
                        const box = [];
                        for (const link of page.$qa("#gdt a")) {
                            const href = link.href;
                            if (processed.has(href)) continue;
                            processed.add(href);
                            box.push(href);
                        }
                        homeData.set(index, box);
                        const display = `[${++task}/${pages}]`;
                        Lib2.title(display);
                        button.$text(`${Transl("獲取頁面")}: ${display}`);
                        if (task === pages) {
                            const box2 = [];
                            for (let index2 = 0; index2 < homeData.size; index2++) {
                                box2.push(...homeData.get(index2));
                            }
                            homeData.clear();
                            processed.clear();
                            Lib2.log(Transl("內頁跳轉數據"), `${comicName}
${JSON.stringify(box2, null, 4)}`, {
                                dev: Config2.Dev
                            });
                            getImageData(box2);
                        }
                    } catch (error) {
                        alert(Transl("請求錯誤重新加載頁面"));
                        location.reload();
                    }
                }
            }
            async function getImageData(homeDataList) {
                const pages = homeDataList.length;
                worker.onmessage = e => {
                    const {
                        index,
                        url: url2,
                        html,
                        time,
                        delay,
                        error
                    } = e.data;
                    error ? worker.postMessage({
                        index: index,
                        url: url2,
                        time: time,
                        delay: dynamicParam(time, delay, null, DConfig2.Image_ND)
                    }) : parseLink(index, url2, Lib2.domParse(html));
                };
                for (const [index, url2] of homeDataList.entries()) {
                    worker.postMessage({
                        index: index,
                        url: url2,
                        time: Date.now(),
                        delay: DConfig2.Image_ID
                    });
                }
                let task = 0;
                const imgData = [];
                function parseLink(index, url2, page) {
                    try {
                        const resample = Lib2.$Q(page, "#img");
                        const original = Lib2.$Q(page, "#i6 div:last-of-type a")?.href || "#";
                        if (!resample) {
                            Lib2.log(null, {
                                page: page,
                                resample: resample,
                                original: original
                            }, {
                                dev: Config2.Dev,
                                type: "error"
                            });
                            throw new Error("Image not found");
                        }
                        const link = Config2.Original && !original.endsWith("#") ? original : resample.src || resample.href;
                        imgData.push({
                            Index: index,
                            PageUrl: url2,
                            ImgUrl: link
                        });
                        const display = `[${++task}/${pages}]`;
                        Lib2.title(display);
                        button.$text(`${Transl("獲取連結")}: ${display}`);
                        if (task === pages) {
                            imgData.sort((a, b) => a.Index - b.Index);
                            Lib2.session(DConfig2.GetKey(), {
                                value: imgData
                            });
                            startTask(imgData);
                        }
                    } catch (error) {
                        Lib2.log(null, error, {
                            dev: Config2.Dev,
                            type: "error"
                        });
                        task++;
                    }
                }
            }
            function reGetImageData(index, url2) {
                function parseLink(index2, url3, page) {
                    const resample = Lib2.$Q(page, "#img");
                    const original = Lib2.$Q(page, "#i6 div:last-of-type a")?.href || "#";
                    if (!resample) return false;
                    const link = Config2.Original && !original.endsWith("#") ? original : resample.src || resample.href;
                    return {
                        Index: index2,
                        PageUrl: url3,
                        ImgUrl: link
                    };
                }
                let token = Config2.ReTry;
                return new Promise(resolve => {
                    worker.postMessage({
                        index: index,
                        url: url2,
                        time: Date.now(),
                        delay: DConfig2.Image_ID
                    });
                    worker.onmessage = e => {
                        const {
                            index: index2,
                            url: url3,
                            html,
                            time,
                            delay,
                            error
                        } = e.data;
                        if (token <= 0) return resolve(false);
                        if (error) {
                            worker.postMessage({
                                index: index2,
                                url: url3,
                                time: time,
                                delay: delay
                            });
                        } else {
                            const result = parseLink(index2, url3, Lib2.domParse(html));
                            if (result) resolve(result); else {
                                worker.postMessage({
                                    index: index2,
                                    url: url3,
                                    time: time,
                                    delay: delay
                                });
                            }
                        }
                        token--;
                    };
                });
            }
            function startTask(dataList) {
                Lib2.log(Transl("圖片連結數據"), `${comicName}
${JSON.stringify(dataList, null, 4)}`, {
                    dev: Config2.Dev
                });
                if (DConfig2.Scope) {
                    dataList = Lib2.scopeParse(DConfig2.Scope, dataList);
                }
                if (DConfig2.SortReverse) {
                    const size = dataList.length - 1;
                    dataList = dataList.map((data, index) => ({
                        ...data,
                        Index: size - index
                    }));
                }
                const dataMap = new Map(dataList.map(data => [data.Index, data]));
                button.$text(Transl("開始下載"));
                Lib2.log(Transl("任務配置"), {
                    ReTry: Config2.ReTry,
                    Original: Config2.Original,
                    ResetScope: Config2.ResetScope,
                    CompleteClose: Config2.CompleteClose,
                    SortReverse: DConfig2.SortReverse,
                    CompressMode: DConfig2.CompressMode,
                    CompressionLevel: DConfig2.Compress_Level,
                    DownloadData: dataMap
                }, {
                    dev: Config2.Dev
                });
                DConfig2.CompressMode ? packDownload(dataMap) : singleDownload(dataMap);
            }
            async function packDownload(dataMap) {
                let totalSize = dataMap.size;
                const fillValue = Lib2.getFill(totalSize);
                let enforce = false;
                let clearCache = false;
                let reTry = Config2.ReTry;
                let task, progress, $thread, $delay;
                function init() {
                    task = 0;
                    progress = 0;
                    $delay = DConfig2.Download_ID;
                    $thread = DConfig2.Download_IT;
                }
                function force() {
                    if (totalSize > 0) {
                        const sortData = [...dataMap].sort((a, b) => a.Index - b.Index);
                        sortData.splice(0, 0, {
                            ErrorPage: sortData.map(([_, value]) => value.Index + 1).join(",")
                        });
                        Lib2.log(Transl("下載失敗數據"), JSON.stringify(sortData, null, 4), {
                            type: "error"
                        });
                    }
                    enforce = true;
                    init();
                    compressFile();
                }
                function runClear() {
                    if (!clearCache) {
                        clearCache = true;
                        sessionStorage.removeItem(DConfig2.GetKey());
                        Lib2.log(Transl("清理警告"), Transl("下載數據不完整將清除緩存, 建議刷新頁面後重載"), {
                            type: "warn"
                        });
                    }
                }
                function statusUpdate(time, index, iurl, blob, error = false) {
                    if (enforce) return;
                    [$delay, $thread] = dynamicParam(time, $delay, $thread, DConfig2.Download_ND);
                    const display = `[${Math.min(++progress, totalSize)}/${totalSize}]`;
                    button?.$text(`${Transl("下載進度")}: ${display}`);
                    Lib2.title(display);
                    if (!error && blob) {
                        zipper.file(`${comicName}/${Lib2.mantissa(index, fillValue, "0", iurl)}`, blob);
                        dataMap.delete(index);
                    }
                    if (progress === totalSize) {
                        totalSize = dataMap.size;
                        if (totalSize > 0 && reTry-- > 0) {
                            const display2 = Transl("等待失敗重試...");
                            Lib2.title(display2);
                            button.$text(display2);
                            setTimeout(() => {
                                start(dataMap, true);
                            }, 2e3);
                        } else force();
                    }
                    --task;
                }
                function request(index, iurl) {
                    if (enforce) return;
                    ++task;
                    let timeout = null;
                    const time = Date.now();
                    if (typeof iurl !== "undefined") {
                        GM_xmlhttpRequest2({
                            url: iurl,
                            timeout: 15e3,
                            method: "GET",
                            responseType: "blob",
                            onload: response => {
                                clearTimeout(timeout);
                                if (response.finalUrl !== iurl && `${response.status}`.startsWith("30")) {
                                    request(index, response.finalUrl);
                                } else {
                                    response.status == 200 ? statusUpdate(time, index, iurl, response.response) : statusUpdate(time, index, iurl, null, true);
                                }
                            },
                            onerror: () => {
                                clearTimeout(timeout);
                                statusUpdate(time, index, iurl, null, true);
                            }
                        });
                    } else {
                        runClear();
                        clearTimeout(timeout);
                        statusUpdate(time, index, iurl, null, true);
                    }
                    timeout = setTimeout(() => {
                        statusUpdate(time, index, iurl, null, true);
                    }, 15e3);
                }
                async function start(dataMap2, reGet = false) {
                    if (enforce) return;
                    init();
                    for (const {
                        Index,
                        PageUrl,
                        ImgUrl
                    } of dataMap2.values()) {
                        if (enforce) break;
                        if (reGet) {
                            Lib2.log(`${Transl("重新取得數據")} (${reTry})`, {
                                Uri: PageUrl
                            }, {
                                dev: Config2.Dev
                            });
                            const result = await reGetImageData(Index, PageUrl);
                            Lib2.log(`${Transl("取得結果")} (${reTry})`, {
                                Result: result
                            }, {
                                dev: Config2.Dev
                            });
                            if (result) {
                                const {
                                    Index: Index2,
                                    ImgUrl: ImgUrl2
                                } = result;
                                request(Index2, ImgUrl2);
                            } else {
                                runClear();
                                request(Index, ImgUrl);
                            }
                        } else {
                            while (task >= $thread) {
                                await Lib2.sleep($delay);
                            }
                            request(Index, ImgUrl);
                        }
                    }
                }
                start(dataMap);
                Lib2.regMenu({
                    [Transl("📥 強制壓縮下載")]: () => force()
                }, {
                    name: "Enforce"
                });
            }
            async function compressFile() {
                Lib2.unMenu("Enforce-1");
                zipper.generateZip({
                    level: DConfig2.Compress_Level
                }, progress => {
                    const display = `${progress.toFixed(1)} %`;
                    Lib2.title(display);
                    button.$text(`${Transl("壓縮進度")}: ${display}`);
                }).then(zip => {
                    saveAs2(zip, `${comicName}.zip`);
                    Lib2.title(`✓ ${DConfig2.TitleCache}`);
                    button.$text(Transl("壓縮完成"));
                    button = null;
                    setTimeout(() => {
                        reset();
                    }, 1500);
                }).catch(result => {
                    Lib2.title(DConfig2.TitleCache);
                    const display = Transl("壓縮失敗");
                    button.$text(display);
                    Lib2.log(display, result, {
                        dev: Config2.Dev,
                        type: "error",
                        collapsed: false
                    });
                    setTimeout(() => {
                        button.disabled = false;
                        button.$text(DConfig2.ModeDisplay);
                        button = null;
                    }, 4500);
                });
            }
            async function singleDownload(dataMap) {
                let totalSize = dataMap.size;
                const fillValue = Lib2.getFill(totalSize);
                const taskPromises = [];
                let task = 0;
                let progress = 0;
                let retryDelay = 1e3;
                let clearCache = false;
                let reTry = Config2.ReTry;
                let $delay = DConfig2.Download_ID;
                let $thread = DConfig2.Download_IT;
                function runClear() {
                    if (!clearCache) {
                        clearCache = true;
                        sessionStorage.removeItem(DConfig2.GetKey());
                        Lib2.log(Transl("清理警告"), Transl("下載數據不完整將清除緩存, 建議刷新頁面後重載"), {
                            type: "warn"
                        });
                    }
                }
                async function request(index, purl, iurl, retry) {
                    return new Promise((resolve, reject) => {
                        if (typeof iurl !== "undefined") {
                            const time = Date.now();
                            ++task;
                            GM_download2({
                                url: iurl,
                                name: `${comicName}-${Lib2.mantissa(index, fillValue, "0", iurl)}`,
                                onload: () => {
                                    [$delay, $thread] = dynamicParam(time, $delay, $thread, DConfig2.Download_ND);
                                    const display = `[${++progress}/${totalSize}]`;
                                    Lib2.title(display);
                                    button?.$text(`${Transl("下載進度")}: ${display}`);
                                    --task;
                                    resolve();
                                },
                                onerror: () => {
                                    if (retry > 0) {
                                        [$delay, $thread] = dynamicParam(time, $delay, $thread, DConfig2.Download_ND);
                                        Lib2.log(null, `[Delay:${$delay}|Thread:${$thread}|Retry:${retry}] : [${iurl}]`, {
                                            dev: Config2.Dev,
                                            type: "error"
                                        });
                                        --task;
                                        setTimeout(() => {
                                            reGetImageData(index, purl).then(({
                                                Index,
                                                PageUrl,
                                                ImgUrl
                                            }) => {
                                                request(Index, PageUrl, ImgUrl, retry - 1);
                                                reject();
                                            }).catch(err => {
                                                runClear();
                                                reject();
                                            });
                                        }, retryDelay += 1e3);
                                    } else {
                                        --task;
                                        reject(new Error("request error"));
                                    }
                                }
                            });
                        } else {
                            runClear();
                            reject();
                        }
                    });
                }
                for (const {
                    Index,
                    PageUrl,
                    ImgUrl
                } of dataMap.values()) {
                    while (task >= $thread) {
                        await Lib2.sleep($delay);
                    }
                    taskPromises.push(request(Index, PageUrl, ImgUrl, reTry));
                }
                await Promise.allSettled(taskPromises);
                button.$text(Transl("下載完成"));
                button = null;
                setTimeout(() => {
                    Lib2.title(`✓ ${DConfig2.TitleCache}`);
                    reset();
                }, 3e3);
            }
        };
    }
    (async () => {
        const eRegex = /https:\/\/e-hentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
        const exRegex = /https:\/\/exhentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
        let Transl, Download;
        let Url = Lib.url.split("?p=")[0];
        async function initStyle() {
            const position = `
            .Download_Button {
                float: right;
                width: 12rem;
                cursor: pointer;
                font-weight: 800;
                line-height: 20px;
                border-radius: 5px;
                position: relative;
                padding: 5px 5px;
                font-family: arial, helvetica, sans-serif;
            }
        `;
            const eStyle = `
            .Download_Button {
            color: #5C0D12;
            border: 2px solid #9a7c7e;
            background-color: #EDEADA;
            }
            .Download_Button:hover {
                color: #8f4701;
                border: 2px dashed #B5A4A4;
            }
            .Download_Button:disabled {
                color: #B5A4A4;
                border: 2px dashed #B5A4A4;
                cursor: default;
                    }
        `;
            const exStyle = `
            .Download_Button {
                color: #b3b3b3;
                border: 2px solid #34353b;
                background-color: #2c2b2b;
            }
            .Download_Button:hover {
                color: #f1f1f1;
                border: 2px dashed #4f535b;
            }
            .Download_Button:disabled {
                color: #4f535b;
                border: 2px dashed #4f535b;
                cursor: default;
            }
        `;
            const style = Lib.$domain === "e-hentai.org" ? eStyle : exStyle;
            Lib.addStyle(`${position}${style}`, "Button-Style");
        }
        async function downloadRangeSetting() {
            const scope = prompt(Transl("範圍設置"));
            if (scope == null) return;
            const yes = confirm(`${Transl("確認設置範圍")}:
${scope}`);
            if (yes) DConfig.Scope = scope;
        }
        async function downloadModeSwitch() {
            DConfig.CompressMode ? Lib.setV("CompressedMode", false) : Lib.setV("CompressedMode", true);
            Lib.$q("#ExDB").remove();
            buttonCreation();
        }
        async function buttonCreation() {
            Lib.waitEl("#gd2", null, {
                raf: true
            }).then(gd2 => {
                DConfig.CompressMode = Lib.getV("CompressedMode", []);
                DConfig.ModeDisplay = DConfig.CompressMode ? Transl("壓縮下載") : Transl("單圖下載");
                const downloadButton = Lib.createElement(gd2, "button", {
                    id: "ExDB",
                    class: "Download_Button",
                    disabled: DConfig.Lock ? true : false,
                    text: DConfig.Lock ? Transl("下載中鎖定") : DConfig.ModeDisplay,
                    on: {
                        type: "click",
                        listener: () => {
                            Download ??= Downloader(GM_xmlhttpRequest, GM_download, Config, DConfig, Transl, Lib, saveAs);
                            DConfig.Lock = true;
                            downloadButton.disabled = true;
                            downloadButton.$text(Transl("開始下載"));
                            Download(Url, downloadButton);
                        },
                        add: {
                            capture: true,
                            passive: true
                        }
                    }
                });
            });
        }
        if (eRegex.test(Url) || exRegex.test(Url)) {
            initStyle();
            DConfig.TitleCache = Lib.title();
            ({
                Transl
            } = (() => {
                const Matcher = Lib.translMatcher(Dict);
                return {
                    Transl: Str => Matcher[Str] ?? Str
                };
            })());
            buttonCreation();
            if (Lib.session(DConfig.GetKey())) {
                const menu = GM_registerMenuCommand(Transl("🚮 清除數據緩存"), () => {
                    sessionStorage.removeItem(DConfig.GetKey());
                    GM_unregisterMenuCommand(menu);
                });
                Lib.regMenu({
                    [Transl("🚮 清除數據緩存")]: () => {
                        sessionStorage.removeItem(DConfig.GetKey());
                        Lib.unMenu("ClearCache-1");
                    }
                }, {
                    name: "ClearCache"
                });
            }
            Lib.regMenu({
                [Transl("🔁 切換下載模式")]: () => downloadModeSwitch(),
                [Transl("⚙️ 下載範圍設置")]: () => downloadRangeSetting()
            });
        }
    })();
})();