[E/Ex-Hentai] Downloader

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

// ==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:en      [E/Ex-Hentai] Downloader
// @version      0.0.16-Beta6
// @author       Canaan HS
// @description         漫畫頁面創建下載按鈕, 可切換 (壓縮下載 | 單圖下載), 無須複雜設置一鍵點擊下載, 自動獲取(非原圖)進行下載
// @description:zh-TW   漫畫頁面創建下載按鈕, 可切換 (壓縮下載 | 單圖下載), 無須複雜設置一鍵點擊下載, 自動獲取(非原圖)進行下載
// @description:zh-CN   漫画页面创建下载按钮, 可切换 (压缩下载 | 单图下载), 无须复杂设置一键点击下载, 自动获取(非原图)进行下载
// @description:ja      マンガページにダウンロードボタンを作成し、(圧缩ダウンロード | シングルイメージダウンロード)を切り替えることができ、复雑な设定は必要なく、ワンクリックでダウンロードできます。自动的に(オリジナルではない)画像を取得してダウンロードします
// @description:ko      만화 페이지에 다운로드 버튼을 만들어 (압축 다운로드 | 단일 이미지 다운로드)를 전환할 수 있으며, 복잡한 설정이 필요하지 않고, 원클릭 다운로드 기능으로 (원본이 아닌) 이미지를 자동으로 가져와 다운로드합니다
// @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      MIT
// @namespace    https://greasyfork.org/users/989635

// @run-at       document-body
// @grant        window.close
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @grant        GM_addElement
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand

// @require      https://update.greasyfork.org/scripts/473358/1237031/JSZip.js
// @require      https://update.greasyfork.org/scripts/495339/1456526/ObjectSyntax_min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==

(async () => {
    /* 使用者配置 */
    const Config = {
        Dev: false,           // 開發模式 (會顯示除錯訊息)
        ReTry: 10,            // 下載錯誤重試次數, 超過這個次數該圖片會被跳過
        Original: false,      // 是否下載原圖
        ResetScope: true,     // 下載完成後 重置範圍設置
        CompleteClose: false, // 下載完成自動關閉
    };
    /* 下載配置 (不清楚不要修改) */
    const DConfig = {
        Compr_Level: 5,
        MIN_CONCURRENCY: 5,
        MAX_CONCURRENCY: 16,
        TIME_THRESHOLD: 1e3,
        MAX_Delay: 3e3,
        Home_ID: 100,
        Home_ND: 80,
        Image_ID: 34,
        Image_ND: 28,
        Download_IT: 6,
        Download_ID: 800,
        Download_ND: 400,
        Lock: false,
        SortReverse: false,
        Scope: undefined,
        DisplayCache: undefined,
        CurrentDownloadMode: undefined,
        KeyCache: undefined,
        GetKey: function () {
            if (!this.KeyCache) this.KeyCache = `DownloadCache_${Syn.Device.Path.split("/").slice(2, 4).join("")}`;
            return this.KeyCache;
        },
        Dynamic: function (Time, Delay, Thread = null, MIN_Delay) {
            let ResponseTime = Date.now() - Time, delay, thread;
            if (ResponseTime > this.TIME_THRESHOLD) {
                delay = Math.floor(Math.min(Delay * 1.1, this.MAX_Delay));
                if (Thread != null) {
                    thread = Math.floor(Math.max(Thread * (this.TIME_THRESHOLD / ResponseTime), this.MIN_CONCURRENCY));
                    return [delay, thread];
                } else {
                    return delay;
                }
            } else {
                delay = Math.ceil(Math.max(Delay * .9, MIN_Delay));
                if (Thread != null) {
                    thread = Math.ceil(Math.min(Thread * 1.2, this.MAX_CONCURRENCY));
                    return [delay, thread];
                } else {
                    return delay;
                }
            }
        }
    };
    const Url = Syn.Device.Url.split("?p=")[0];
    let Lang, OriginalTitle, CompressMode, ModeDisplay;
    class DownloadCore {
        constructor(Button) {
            this.Button = Button;
            this.ComicName = null;
            this.Worker = Syn.WorkerCreation(`
                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, time, delay, error: true});
                    }
                }
            `);
            this.GetTotal = page => Math.ceil(+page[page.length - 2].textContent.replace(/\D/g, "") / 20);
            this.GetHomeData();
        }
        async Reset() {
            Config.CompleteClose && window.close();
            Config.ResetScope && (DConfig.Scope = false);
            const Button = Syn.$$("#ExDB");
            DConfig.Lock = false;
            Button.disabled = false;
            Button.textContent = `✓ ${ModeDisplay}`;
        }
        async GetHomeData() {
            const Name = Syn.NameFilter((Syn.$$("#gj").textContent || Syn.$$("#gn").textContent).trim());
            const CacheData = Syn.Storage(DConfig.GetKey());
            const ImgSet = Syn.$$("#gdc .ct6");
            DConfig.CurrentDownloadMode = CompressMode;
            this.ComicName = Name;
            if (ImgSet) {
                const yes = confirm(Lang.Transl("檢測到圖片集 !!\n\n是否反轉排序後下載 ?"));
                yes ? DConfig.SortReverse = true : DConfig.SortReverse = false;
            }
            if (CacheData) {
                this.StartTask(CacheData);
                return;
            }
            const Pages = this.GetTotal(Syn.$$("#gdd td.gdt2", {
                all: true
            }));
            let Delay = DConfig.Home_ID;
            this.Worker.postMessage({
                index: 0,
                url: Url,
                time: Date.now(),
                delay: Delay
            });
            for (let index = 1; index < Pages; index++) {
                this.Worker.postMessage({
                    index: index,
                    url: `${Url}?p=${index}`,
                    time: Date.now(),
                    delay: Delay
                });
            }
            this.Worker.onmessage = e => {
                const {
                    index,
                    url,
                    html,
                    time,
                    delay,
                    error
                } = e.data;
                Delay = DConfig.Dynamic(time, delay, null, DConfig.Home_ND);
                error ? this.Worker.postMessage({
                    index: index,
                    url: url,
                    time: time,
                    delay: delay
                }) : GetLink(index, Syn.DomParse(html));
            };
            const self = this;
            const HomeData = new Map();
            let Task = 0;
            function GetLink(index, page) {
                try {
                    const Cache = [];
                    for (const link of Syn.$$("#gdt a", {
                        all: true,
                        root: page
                    })) {
                        Cache.push(link.href);
                    }
                    HomeData.set(index, Cache);
                    DConfig.DisplayCache = `[${++Task}/${Pages}]`;
                    document.title = DConfig.DisplayCache;
                    self.Button.textContent = `${Lang.Transl("獲取頁面")}: ${DConfig.DisplayCache}`;
                    if (Task === Pages) {
                        const Cache = [];
                        for (let index = 0; index < HomeData.size; index++) {
                            Cache.push(...HomeData.get(index));
                        }
                        const Processed = [...new Set(Cache)];
                        Syn.Log(Lang.Transl("內頁跳轉數據"), `${Name}\n${JSON.stringify(Processed, null, 4)}`, {
                            dev: Config.Dev
                        });
                        self.GetImageData(Processed);
                    }
                } catch (error) {
                    alert(Lang.Transl("請求錯誤重新加載頁面"));
                    location.reload();
                }
            }
        }
        async GetImageData(JumpList) {
            const Pages = JumpList.length;
            let Delay = DConfig.Image_ID;
            let Task = 0;
            for (let index = 0; index < Pages; index++) {
                this.Worker.postMessage({
                    index: index,
                    url: JumpList[index],
                    time: Date.now(),
                    delay: Delay
                });
            }
            this.Worker.onmessage = e => {
                const {
                    index,
                    url,
                    html,
                    time,
                    delay,
                    error
                } = e.data;
                Delay = DConfig.Dynamic(time, delay, null, DConfig.Image_ND);
                error ? this.Worker.postMessage({
                    index: index,
                    url: url,
                    time: time,
                    delay: delay
                }) : GetLink(index, url, Syn.DomParse(html));
            };
            const self = this;
            const ImageData = [];
            function GetLink(index, url, page) {
                try {
                    const Resample = Syn.$$("#img", {
                        root: page
                    });
                    const Original = Syn.$$("#i6 div:last-of-type a", {
                        root: page
                    });
                    if (!Resample) {
                        self.Worker.postMessage({
                            index: index,
                            url: url,
                            time: Date.now(),
                            delay: Delay
                        });
                        return;
                    }
                    const Link = Config.Original ? Original.href ?? Resample.src ?? Resample.href : Resample.src ?? Resample.href;
                    ImageData.push([index, {
                        PageUrl: url,
                        ImageUrl: Link
                    }]);
                    DConfig.DisplayCache = `[${++Task}/${Pages}]`;
                    document.title = DConfig.DisplayCache;
                    self.Button.textContent = `${Lang.Transl("獲取連結")}: ${DConfig.DisplayCache}`;
                    if (Task === Pages) {
                        ImageData.sort((a, b) => a[0] - b[0]);
                        const Processed = new Map(ImageData);
                        Syn.Storage(DConfig.GetKey(), {
                            value: Processed
                        });
                        self.StartTask(Processed);
                    }
                } catch (error) {
                    Syn.Log(null, error, {
                        dev: Config.Dev,
                        type: "error"
                    });
                    Task++;
                }
            }
        }
        ReGetImageData(Index, Url) {
            function GetLink(index, url, page) {
                const Resample = Syn.$$("#img", {
                    root: page
                });
                const Original = Syn.$$("#i6 div:last-of-type a", {
                    root: page
                });
                if (!Resample) return false;
                const Link = Config.Original ? Original.href ?? Resample.src ?? Resample.href : Resample.src ?? Resample.href;
                return [index, url, Link];
            }
            let Token = Config.ReTry;
            return new Promise((resolve, reject) => {
                this.Worker.postMessage({
                    index: Index,
                    url: Url,
                    time: Date.now(),
                    delay: DConfig.Image_ID
                });
                this.Worker.onmessage = e => {
                    const {
                        index,
                        url,
                        html,
                        time,
                        delay,
                        error
                    } = e.data;
                    if (error) {
                        this.Worker.postMessage({
                            Index: Index,
                            url: Url,
                            time: time,
                            delay: delay
                        });
                    } else {
                        if (Token <= 0) reject(false);
                        const result = GetLink(index, url, Syn.DomParse(html));
                        if (result) resolve(result); else {
                            this.Worker.postMessage({
                                Index: Index,
                                url: Url,
                                time: time,
                                delay: delay
                            });
                            Token - 1;
                        }
                    }
                };
            });
        }
        StartTask(DataMap) {
            Syn.Log(Lang.Transl("圖片連結數據"), `${this.ComicName}\n${JSON.stringify([...DataMap], null, 4)}`, {
                dev: Config.Dev
            });
            if (DConfig.Scope) {
                DataMap = new Map(Syn.ScopeParsing(DConfig.Scope, [...DataMap]));
            }
            if (DConfig.SortReverse) {
                const Size = DataMap.size - 1;
                DataMap = new Map([...DataMap.entries()].map(([index, url]) => [Size - index, url]));
            }
            Syn.Log(Lang.Transl("任務配置"), {
                ReTry: Config.ReTry,
                Original: Config.Original,
                ResetScope: Config.ResetScope,
                CompleteClose: Config.CompleteClose,
                SortReverse: DConfig.SortReverse,
                DownloadMode: DConfig.CurrentDownloadMode,
                CompressionLevel: DConfig.Compr_Level
            }, {
                dev: Config.Dev
            });
            this.Button.textContent = Lang.Transl("開始下載");
            DConfig.CurrentDownloadMode ? this.PackDownload(DataMap) : this.SingleDownload(DataMap);
        }
        async PackDownload(Data) {
            const self = this;
            const Zip = new JSZip();
            let Total = Data.size;
            const Fill = Syn.GetFill(Total);
            let Enforce = false;
            let ClearCache = false;
            let ReTry = Config.ReTry;
            let Task, Progress, Thread, Delay;
            function Init() {
                Task = 0;
                Progress = 0;
                Delay = DConfig.Download_ID;
                Thread = DConfig.Download_IT;
            }
            function Force() {
                if (Total > 0) {
                    const SortData = [...Data].sort((a, b) => a[0] - b[0]);
                    SortData.splice(0, 0, {
                        ErrorPage: SortData.map(item => ++item[0]).join(",")
                    });
                    Syn.Log(Lang.Transl("下載失敗數據"), JSON.stringify(SortData, null, 4), {
                        type: "error"
                    });
                }
                Enforce = true;
                Init();
                self.Compression(Zip);
            }
            function RunClear() {
                if (!ClearCache) {
                    ClearCache = true;
                    sessionStorage.removeItem(DConfig.GetKey());
                    Syn.Log(Lang.Transl("清理警告"), Lang.Transl("下載數據不完整將清除緩存, 建議刷新頁面後重載"), {
                        type: "warn"
                    });
                }
            }
            function StatusUpdate(time, index, purl, iurl, blob, error = false) {
                if (Enforce) return;
                [Delay, Thread] = DConfig.Dynamic(time, Delay, Thread, DConfig.Download_ND);
                Data.delete(index);
                if (error && typeof iurl === "string") {
                    Data.set(index, {
                        PageUrl: purl,
                        ImageUrl: iurl
                    });
                } else {
                    Zip.file(`${self.ComicName}/${Syn.Mantissa(index, Fill, "0", iurl)}`, blob);
                }
                DConfig.DisplayCache = `[${++Progress}/${Total}]`;
                document.title = DConfig.DisplayCache;
                self.Button && (self.Button.textContent = `${Lang.Transl("下載進度")}: ${DConfig.DisplayCache}`);
                --Task;
                if (Progress === Total) {
                    Total = Data.size;
                    if (Total > 0 && ReTry-- > 0) {
                        DConfig.DisplayCache = Lang.Transl("等待失敗重試...");
                        document.title = DConfig.DisplayCache;
                        self.Button.textContent = DConfig.DisplayCache;
                        setTimeout(() => { Start(Data, true) }, 2e3);
                    } else Force();
                } else if (Progress > Total) Init();
            }
            function Request(Index, Purl, Iurl) {
                if (Enforce) return;
                if (typeof Iurl !== "undefined") {
                    const time = Date.now();
                    let timeout = null;
                    ++Task;
                    GM_xmlhttpRequest({
                        url: Iurl,
                        timeout: 2e4,
                        method: "GET",
                        responseType: "blob",
                        onload: response => {
                            clearTimeout(timeout);
                            const blob = response.response;
                            response.status == 200 && blob instanceof Blob && blob.size > 0 ? StatusUpdate(time, Index, Purl, Iurl, blob) : StatusUpdate(time, Index, Purl, Iurl, null, true);
                        },
                        onerror: () => {
                            clearTimeout(timeout);
                            StatusUpdate(time, Index, Purl, Iurl, null, true);
                        }
                    });
                    timeout = setTimeout(() => {
                        StatusUpdate(time, Index, Purl, Iurl, null, true);
                    }, 2e4);
                } else {
                    RunClear();
                    StatusUpdate(time, Index, null, null, true);
                }
            }
            async function Start(DataMap, ReGet = false) {
                if (Enforce) return;
                Init();
                for (const [Index, Uri] of DataMap.entries()) {
                    if (Enforce) break;
                    if (ReGet) {
                        Syn.Log(Lang.Transl("重新取得數據"), {
                            Uri: Uri.PageUrl
                        }, {
                            dev: Config.Dev
                        });
                        const Result = await self.ReGetImageData(Index, Uri.PageUrl);
                        Syn.Log(Lang.Transl("取得結果"), {
                            Result: Result
                        }, {
                            dev: Config.Dev
                        });
                        if (Result) {
                            const [Index, Purl, Iurl] = Result;
                            Request(Index, Purl, Iurl);
                        } else {
                            RunClear();
                            Request(Index, Uri.PageUrl, Uri.ImageUrl);
                        }
                    } else {
                        while (Task >= Thread) {
                            await Syn.Sleep(Delay);
                        }
                        Request(Index, Uri.PageUrl, Uri.ImageUrl);
                    }
                }
            }
            Start(Data);
            Syn.Menu({
                [Lang.Transl("📥 強制壓縮下載")]: {
                    func: () => Force(),
                    hotkey: "d"
                }
            }, "Enforce");
        }
        async SingleDownload(Data) {
            const self = this;
            let Total = Data.size;
            const Fill = Syn.GetFill(Total);
            const TaskPromises = [];
            let Task = 0;
            let Progress = 0;
            let RetryDelay = 1e3;
            let ClearCache = false;
            let ReTry = Config.ReTry;
            let Delay = DConfig.Download_ID;
            let Thread = DConfig.Download_IT;
            function RunClear() {
                if (!ClearCache) {
                    ClearCache = true;
                    sessionStorage.removeItem(DConfig.GetKey());
                    Syn.Log(Lang.Transl("清理警告"), Lang.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_download({
                            url: Iurl,
                            name: `${self.ComicName}-${Syn.Mantissa(Index, Fill, "0", Iurl)}`,
                            onload: () => {
                                [Delay, Thread] = DConfig.Dynamic(time, Delay, Thread, DConfig.Download_ND);
                                DConfig.DisplayCache = `[${++Progress}/${Total}]`;
                                document.title = DConfig.DisplayCache;
                                self.Button && (self.Button.textContent = `${Lang.Transl("下載進度")}: ${DConfig.DisplayCache}`);
                                --Task;
                                resolve();
                            },
                            onerror: () => {
                                if (Retry > 0) {
                                    [Delay, Thread] = DConfig.Dynamic(time, Delay, Thread, DConfig.Download_ND);
                                    Syn.Log(null, `[Delay:${Delay}|Thread:${Thread}|Retry:${Retry}] : [${Iurl}]`, {
                                        dev: Config.Dev,
                                        type: "error"
                                    });
                                    --Task;
                                    setTimeout(() => {
                                        self.ReGetImageData(Index, Purl).then(data => {
                                            const [Index, Purl, Iurl] = data;
                                            Request(Index, Purl, Iurl, Retry - 1);
                                            reject();
                                        }).catch(err => {
                                            RunClear();
                                            reject();
                                        });
                                    }, RetryDelay += 1e3);
                                } else {
                                    --Task;
                                    reject(new Error("Request error"));
                                }
                            }
                        });
                    } else {
                        RunClear();
                        reject();
                    }
                });
            }
            for (const [Index, Uri] of Data.entries()) {
                while (Task >= Thread) {
                    await Syn.Sleep(Delay);
                }
                TaskPromises.push(Request(Index, Uri.PageUrl, Uri.ImageUrl, ReTry));
            }
            await Promise.allSettled(TaskPromises);
            this.Button.textContent = Lang.Transl("下載完成");
            this.Button = null;
            setTimeout(() => {
                document.title = `✓ ${OriginalTitle}`;
                this.Reset();
            }, 3e3);
        }
        async Compression(Zip) {
            const self = this;
            GM_unregisterMenuCommand("Enforce-1");
            function ErrorProcess(result) {
                document.title = OriginalTitle;
                DConfig.DisplayCache = Lang.Transl("壓縮失敗");
                self.Button.textContent = DConfig.DisplayCache;
                Syn.Log(DConfig.DisplayCache, result, {
                    dev: Config.Dev,
                    type: "error",
                    collapsed: false
                });
                setTimeout(() => {
                    self.Button.disabled = false;
                    self.Button.textContent = ModeDisplay;
                    self.Button = null;
                }, 4500);
            }
            if (Object.keys(Zip.files).length == 0) {
                ErrorProcess("無數據可壓縮");
                return;
            }
            Zip.generateAsync({
                type: "blob",
                compression: "DEFLATE",
                compressionOptions: {
                    level: DConfig.Compr_Level
                }
            }, progress => {
                DConfig.DisplayCache = `${progress.percent.toFixed(1)} %`;
                document.title = DConfig.DisplayCache;
                this.Button.textContent = `${Lang.Transl("壓縮進度")}: ${DConfig.DisplayCache}`;
            }).then(zip => {
                saveAs(zip, `${this.ComicName}.zip`);
                document.title = `✓ ${OriginalTitle}`;
                this.Button.textContent = Lang.Transl("壓縮完成");
                this.Button = null;
                setTimeout(() => {
                    this.Reset();
                }, 3e3);
            }).catch(result => {
                ErrorProcess(result);
            });
        }
    }
    class ButtonCore {
        constructor() {
            this.E = /https:\/\/e-hentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
            this.Ex = /https:\/\/exhentai\.org\/g\/\d+\/[a-zA-Z0-9]+/;
            this.Allow = (Uri = Url) => this.E.test(Uri) || this.Ex.test(Uri);
            this.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 E_Style = `
                    .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 Ex_Style = `
                    .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 = Syn.Device.Host === "e-hentai.org" ? E_Style : Ex_Style;
                Syn.AddStyle(`${Position}${Style}`, "Button-style", false);
            };
        }
        async DownloadModeSwitch() {
            CompressMode ? Syn.Store("s", "CompressedMode", false) : Syn.Store("s", "CompressedMode", true);
            Syn.$$("#ExDB").remove();
            this.ButtonCreation();
        }
        async DownloadRangeSetting() {
            let scope = prompt(Lang.Transl("範圍設置")) || false;
            if (scope) {
                const yes = confirm(`${Lang.Transl("確認設置範圍")}:\n${scope}`);
                if (yes) DConfig.Scope = scope;
            }
        }
        async ButtonCreation() {
            CompressMode = Syn.Store("g", "CompressedMode", []);
            ModeDisplay = CompressMode ? Lang.Transl("壓縮下載") : Lang.Transl("單圖下載");
            const download_button = GM_addElement(Syn.$$("#gd2"), "button", {
                id: "ExDB",
                class: "Download_Button"
            });
            download_button.disabled = DConfig.Lock ? true : false;
            download_button.textContent = DConfig.Lock ? Lang.Transl("下載中鎖定") : ModeDisplay;
            Syn.AddListener(download_button, "click", () => {
                DConfig.Lock = true;
                download_button.disabled = true;
                download_button.textContent = Lang.Transl("開始下載");
                this.TaskInstance = new DownloadCore(download_button);
            }, {
                capture: true,
                passive: true
            });
        }
        static async Init() {
            const Core = new ButtonCore();
            if (Core.Allow()) {
                Core.InitStyle();
                OriginalTitle = document.title;
                Lang = Language(Syn.Device.Lang);
                Core.ButtonCreation();
                if (Syn.Storage(DConfig.GetKey())) {
                    const menu = GM_registerMenuCommand(Lang.Transl("🚮 清除數據緩存"), () => {
                        sessionStorage.removeItem(DConfig.GetKey());
                        GM_unregisterMenuCommand(menu);
                    });
                }
                Syn.Menu({
                    [Lang.Transl("🔁 切換下載模式")]: {
                        func: () => Core.DownloadModeSwitch()
                    },
                    [Lang.Transl("⚙️ 下載範圍設置")]: {
                        func: () => Core.DownloadRangeSetting()
                    }
                });
            }
        }
    }
    function Language(lang) {
        const Word = {
            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"
            },
            English: {
                "🚮 清除數據緩存": "🚮 Clear data cache",
                "🔁 切換下載模式": "🔁 Switch download mode",
                "⚙️ 下載範圍設置": "⚙️ Download range settings",
                "📥 強制壓縮下載": "📥 Force compressed download",
                "⛔️ 終止下載": "⛔️ Terminate download",
                "壓縮下載": "Compressed download",
                "單圖下載": "Single image download",
                "下載中鎖定": "Locked during download",
                "開始下載": "Start download",
                "獲取頁面": "Fetch page",
                "獲取連結": "Fetch link",
                "下載進度": "Download progress",
                "壓縮進度": "Compression progress",
                "壓縮完成": "Compression complete",
                "壓縮失敗": "Compression failed",
                "下載完成": "Download complete",
                "清理警告": "Clean up warning",
                "任務配置": "Task Configuration",
                "取得結果": "Fetch Results",
                "重新取得數據": "Refetch Data",
                "確認設置範圍": "Confirm range settings",
                "剩餘重載次數": "Remaining reload attempts",
                "下載失敗數據": "Failed download data",
                "內頁跳轉數據": "Inner page redirection data",
                "圖片連結數據": "Image link data",
                "等待失敗重試...": "Waiting for failed retry...",
                "請求錯誤重新加載頁面": "Request error, reload the page",
                "檢測到圖片集 !!\n\n是否反轉排序後下載 ?": "Image collection detected !!\n\nWould you like to reverse the order and download?",
                "下載數據不完整將清除緩存, 建議刷新頁面後重載": "Download data is incomplete, cache will be cleared, it's recommended to refresh the page and reload",
                "找不到圖片元素, 你的 IP 可能被禁止了, 請刷新頁面重試": "Image element not found, your IP might be blocked, please refresh the page and try again",
                "範圍設置": "Automatically reset after download completion\n\nSingle item settings: 1. 2, 3\nRange settings: 1~5, 6-10\nExclusion settings: !5, -10\n"
            },
            Korea: {
                "🚮 清除數據緩存": "🚮 데이터 캐시 삭제",
                "🔁 切換下載模式": "🔁 다운로드 모드 전환",
                "⚙️ 下載範圍設置": "⚙️ 다운로드 범위 설정",
                "📥 強制壓縮下載": "📥 강제 압축 다운로드",
                "⛔️ 終止下載": "⛔️ 다운로드 중단",
                "壓縮下載": "압축 다운로드",
                "單圖下載": "단일 이미지 다운로드",
                "下載中鎖定": "다운로드 중 잠금",
                "開始下載": "다운로드 시작",
                "獲取頁面": "페이지 가져오기",
                "獲取連結": "링크 가져오기",
                "下載進度": "다운로드 진행",
                "壓縮進度": "압축 진행",
                "壓縮完成": "압축 완료",
                "壓縮失敗": "압축 실패",
                "下載完成": "다운로드 완료",
                "清理警告": "경고 정리",
                "任務配置": "작업 설정",
                "取得結果": "결과 가져오기",
                "重新取得數據": "데이터 다시 가져오기",
                "確認設置範圍": "설정 범위 확인",
                "剩餘重載次數": "남은 재시도 횟수",
                "下載失敗數據": "다운로드 실패 데이터",
                "內頁跳轉數據": "내부 페이지 리디렉션 데이터",
                "圖片連結數據": "이미지 링크 데이터",
                "等待失敗重試...": "실패 재시도를 기다리는 중...",
                "請求錯誤重新加載頁面": "요청 오류, 페이지를 다시 로드하십시오",
                "檢測到圖片集 !!\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範囲設定: 1~5, 6-10\n除外設定: !5, -10\n"
            }
        }, Match = {
            ko: Word.Korea,
            ja: Word.Japan,
            "en-US": Word.English,
            "zh-CN": Word.Simplified,
            "zh-SG": Word.Simplified,
            "zh-TW": Word.Traditional,
            "zh-HK": Word.Traditional,
            "zh-MO": Word.Traditional
        }, ML = Match[lang] ?? Match["en-US"];
        return {
            Transl: Str => ML[Str] ?? Str
        };
    }
    ButtonCore.Init();
})();