IwaraZip Enhancement

Enhancement IwaraZip

// ==UserScript==
// @name              IwaraZip Enhancement
// @description       Enhancement IwaraZip
// @name:zh-CN        IwaraZip 增强
// @description:zh-CN 增强 IwaraZip 使用体验
// @icon              https://www.iwara.zip/themes/spirit/assets/images/favicon/favicon.ico
// @namespace         https://github.com/dawn-lc/
// @author            dawn-lc
// @license           Apache-2.0
// @copyright         2024, Dawnlc (https://dawnlc.me/)
// @source            https://github.com/dawn-lc/IwaraZipEnhancement
// @supportURL        https://github.com/dawn-lc/IwaraZipEnhancement/issues
// @connect           iwara.zip
// @connect           *.iwara.zip
// @connect           localhost
// @connect           127.0.0.1
// @connect           *
// @match             *://*.iwara.zip/*
// @grant             GM_getValue
// @grant             GM_setValue
// @grant             GM_listValues
// @grant             GM_deleteValue
// @grant             GM_addValueChangeListener
// @grant             GM_addStyle
// @grant             GM_addElement
// @grant             GM_getResourceText
// @grant             GM_setClipboard
// @grant             GM_download
// @grant             GM_xmlhttpRequest
// @grant             GM_openInTab
// @grant             GM_info
// @grant             unsafeWindow
// @run-at            document-start
// @require           https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js
// @resource          toastify-css https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.css
// @version           0.1.15
// ==/UserScript==
(function () {
    const originalFetch = unsafeWindow.fetch;
    const originalNodeAppendChild = unsafeWindow.Node.prototype.appendChild;
    const originalAddEventListener = unsafeWindow.EventTarget.prototype.addEventListener;
    const isNull = (obj) => typeof obj === 'undefined' || obj === null;
    const isObject = (obj) => !isNull(obj) && typeof obj === 'object' && !Array.isArray(obj);
    const isString = (obj) => !isNull(obj) && typeof obj === 'string';
    const isNumber = (obj) => !isNull(obj) && typeof obj === 'number';
    const isElement = (obj) => !isNull(obj) && obj instanceof Element;
    const isNode = (obj) => !isNull(obj) && obj instanceof Node;
    const isStringTupleArray = (obj) => Array.isArray(obj) && obj.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string');
    const hasFunction = (obj, method) => {
        return !method.isEmpty() && !isNull(obj) ? method in obj && typeof obj[method] === 'function' : false;
    };
    const getString = (obj) => {
        obj = obj instanceof Error ? String(obj) : obj;
        obj = obj instanceof Date ? obj.format('YYYY-MM-DD') : obj;
        return typeof obj === 'object' ? JSON.stringify(obj, null, 2) : String(obj);
    };
    Array.prototype.any = function () {
        return this.prune().length > 0;
    };
    Array.prototype.prune = function () {
        return this.filter(i => i !== null && typeof i !== 'undefined');
    };
    Array.prototype.unique = function (prop) {
        return this.filter((item, index, self) => index === self.findIndex((t) => (prop ? t[prop] === item[prop] : t === item)));
    };
    Array.prototype.union = function (that, prop) {
        return [...this, ...that].unique(prop);
    };
    Array.prototype.intersect = function (that, prop) {
        return this.filter((item) => that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
    };
    Array.prototype.difference = function (that, prop) {
        return this.filter((item) => !that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
    };
    Array.prototype.complement = function (that, prop) {
        return this.union(that, prop).difference(this.intersect(that, prop), prop);
    };
    String.prototype.isEmpty = function () {
        return !isNull(this) && this.length === 0;
    };
    String.prototype.among = function (start, end, greedy = false) {
        if (this.isEmpty() || start.isEmpty() || end.isEmpty())
            return '';
        const startIndex = this.indexOf(start);
        if (startIndex === -1)
            return '';
        const adjustedStartIndex = startIndex + start.length;
        const endIndex = greedy ? this.lastIndexOf(end) : this.indexOf(end, adjustedStartIndex);
        if (endIndex === -1 || endIndex < adjustedStartIndex)
            return '';
        return this.slice(adjustedStartIndex, endIndex);
    };
    String.prototype.splitLimit = function (separator, limit) {
        if (this.isEmpty() || isNull(separator)) {
            throw new Error('Empty');
        }
        let body = this.split(separator);
        return limit ? body.slice(0, limit).concat(body.slice(limit).join(separator)) : body;
    };
    String.prototype.truncate = function (maxLength) {
        return this.length > maxLength ? this.substring(0, maxLength) : this.toString();
    };
    String.prototype.trimHead = function (prefix) {
        return this.startsWith(prefix) ? this.slice(prefix.length) : this.toString();
    };
    String.prototype.trimTail = function (suffix) {
        return this.endsWith(suffix) ? this.slice(0, -suffix.length) : this.toString();
    };
    String.prototype.toURL = function () {
        let URLString = this;
        if (URLString.split('//')[0].isEmpty()) {
            URLString = `${unsafeWindow.location.protocol}${URLString}`;
        }
        return new URL(URLString.toString());
    };
    Array.prototype.append = function (arr) {
        this.push(...arr);
    };
    String.prototype.replaceVariable = function (replacements, count = 0) {
        let replaceString = this.toString();
        try {
            replaceString = Object.entries(replacements).reduce((str, [key, value]) => {
                if (str.includes(`%#${key}:`)) {
                    let format = str.among(`%#${key}:`, '#%').toString();
                    return str.replaceAll(`%#${key}:${format}#%`, getString(hasFunction(value, 'format') ? value.format(format) : value));
                }
                else {
                    return str.replaceAll(`%#${key}#%`, getString(value));
                }
            }, replaceString);
            count++;
            return Object.keys(replacements).map((key) => this.includes(`%#${key}#%`)).includes(true) && count < 128 ? replaceString.replaceVariable(replacements, count) : replaceString;
        }
        catch (error) {
            GM_getValue('isDebug') && console.log(`replace variable error: ${getString(error)}`);
            return replaceString;
        }
    };
    function prune(obj) {
        if (Array.isArray(obj)) {
            return obj.filter(isNotEmpty).map(prune);
        }
        if (isElement(obj) || isNode(obj)) {
            return obj;
        }
        if (isObject(obj)) {
            return Object.fromEntries(Object.entries(obj)
                .filter(([key, value]) => isNotEmpty(value))
                .map(([key, value]) => [key, prune(value)]));
        }
        return isNotEmpty(obj) ? obj : undefined;
    }
    function isNotEmpty(obj) {
        if (isNull(obj)) {
            return false;
        }
        if (Array.isArray(obj)) {
            return obj.some(isNotEmpty);
        }
        if (isString(obj)) {
            return !obj.isEmpty();
        }
        if (isNumber(obj)) {
            return !Number.isNaN(obj);
        }
        if (isElement(obj) || isNode(obj)) {
            return true;
        }
        if (isObject(obj)) {
            return Object.values(obj).some(isNotEmpty);
        }
        return true;
    }
    const fetch = (input, init, force) => {
        if (init && init.headers && isStringTupleArray(init.headers))
            throw new Error("init headers Error");
        if (init && init.method && !(init.method === 'GET' || init.method === 'HEAD' || init.method === 'POST'))
            throw new Error("init method Error");
        return force || (typeof input === 'string' ? input : input.url).toURL().hostname !== unsafeWindow.location.hostname ? new Promise((resolve, reject) => {
            GM_xmlhttpRequest(prune({
                method: (init && init.method) || 'GET',
                url: typeof input === 'string' ? input : input.url,
                headers: (init && init.headers) || {},
                data: ((init && init.body) || null),
                onload: function (response) {
                    resolve(new Response(response.responseText, {
                        status: response.status,
                        statusText: response.statusText,
                    }));
                },
                onerror: function (error) {
                    reject(error);
                }
            }));
        }) : originalFetch(input, init);
    };
    const UUID = function () {
        return Array.from({ length: 8 }, () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)).join('');
    };
    const ceilDiv = function (dividend, divisor) {
        return Math.floor(dividend / divisor) + (dividend % divisor > 0 ? 1 : 0);
    };
    const language = function () {
        let env = (!isNull(config) ? config.language : (navigator.language ?? navigator.languages[0] ?? 'en')).replace('-', '_');
        let main = env.split('_').shift() ?? 'en';
        return (!isNull(i18n[env]) ? env : !isNull(i18n[main]) ? main : 'en');
    };
    const renderNode = function (renderCode) {
        renderCode = prune(renderCode);
        if (isNull(renderCode))
            throw new Error("RenderCode null");
        if (typeof renderCode === 'string') {
            return document.createTextNode(renderCode.replaceVariable(i18n[language()]).toString());
        }
        if (renderCode instanceof Node) {
            return renderCode;
        }
        if (typeof renderCode !== 'object' || !renderCode.nodeType) {
            throw new Error('Invalid arguments');
        }
        const { nodeType, attributes, events, className, childs } = renderCode;
        const node = document.createElement(nodeType);
        (!isNull(attributes) && Object.keys(attributes).any()) && Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, value));
        (!isNull(events) && Object.keys(events).any()) && Object.entries(events).forEach(([eventName, eventHandler]) => originalAddEventListener.call(node, eventName, eventHandler));
        (!isNull(className) && className.length > 0) && node.classList.add(...[].concat(className));
        !isNull(childs) && node.append(...[].concat(childs).map(renderNode));
        return node;
    };
    let ToastType;
    (function (ToastType) {
        ToastType[ToastType["Log"] = 0] = "Log";
        ToastType[ToastType["Info"] = 1] = "Info";
        ToastType[ToastType["Warn"] = 2] = "Warn";
        ToastType[ToastType["Error"] = 3] = "Error";
    })(ToastType || (ToastType = {}));
    let VersionState;
    (function (VersionState) {
        VersionState[VersionState["Low"] = 0] = "Low";
        VersionState[VersionState["Equal"] = 1] = "Equal";
        VersionState[VersionState["High"] = 2] = "High";
    })(VersionState || (VersionState = {}));
    class Version {
        constructor(versionString) {
            const [version, preRelease, buildMetadata] = versionString.split(/[-+]/);
            const versionParts = version.split('.').map(Number);
            this.major = versionParts[0] || 0;
            this.minor = versionParts.length > 1 ? versionParts[1] : 0;
            this.patch = versionParts.length > 2 ? versionParts[2] : 0;
            this.preRelease = preRelease ? preRelease.split('.') : [];
            this.buildMetadata = buildMetadata;
        }
        compare(other) {
            const compareSegment = (a, b) => {
                if (a < b) {
                    return VersionState.Low;
                }
                else if (a > b) {
                    return VersionState.High;
                }
                return VersionState.Equal;
            };
            let state = compareSegment(this.major, other.major);
            if (state !== VersionState.Equal)
                return state;
            state = compareSegment(this.minor, other.minor);
            if (state !== VersionState.Equal)
                return state;
            state = compareSegment(this.patch, other.patch);
            if (state !== VersionState.Equal)
                return state;
            for (let i = 0; i < Math.max(this.preRelease.length, other.preRelease.length); i++) {
                const pre1 = this.preRelease[i];
                const pre2 = other.preRelease[i];
                if (pre1 === undefined && pre2 !== undefined) {
                    return VersionState.High;
                }
                else if (pre1 !== undefined && pre2 === undefined) {
                    return VersionState.Low;
                }
                if (pre1 !== undefined && pre2 !== undefined) {
                    state = compareSegment(isNaN(+pre1) ? pre1 : +pre1, isNaN(+pre2) ? pre2 : +pre2);
                    if (state !== VersionState.Equal)
                        return state;
                }
            }
            return VersionState.Equal;
        }
    }
    if (GM_getValue('isDebug')) {
        console.log(getString(GM_info));
        debugger;
    }
    let DownloadType;
    (function (DownloadType) {
        DownloadType[DownloadType["Aria2"] = 0] = "Aria2";
        DownloadType[DownloadType["Browser"] = 1] = "Browser";
        DownloadType[DownloadType["Others"] = 2] = "Others";
    })(DownloadType || (DownloadType = {}));
    class I18N {
        constructor() {
            this.zh_CN = this['zh'];
            this.zh = {
                appName: 'IwaraZip 增强',
                language: '语言: ',
                downloadPath: '下载到: ',
                downloadProxy: '下载代理: ',
                downloadProxyUser: '代理用户名: ',
                downloadProxyPassword: '下载密码: ',
                aria2Path: 'Aria2 RPC: ',
                aria2Token: 'Aria2 密钥: ',
                rename: '重命名',
                save: '保存',
                reset: '重置',
                ok: '确定',
                on: '开启',
                off: '关闭',
                isDebug: '调试模式',
                downloadType: '下载方式',
                browserDownload: '浏览器下载',
                configurationIncompatible: '检测到不兼容的配置文件,请重新配置!',
                browserDownloadNotEnabled: `未启用下载功能!`,
                browserDownloadNotWhitelisted: `请求的文件扩展名未列入白名单!`,
                browserDownloadNotPermitted: `下载功能已启用,但未授予下载权限!`,
                browserDownloadNotSupported: `目前浏览器/版本不支持下载功能!`,
                browserDownloadNotSucceeded: `下载未开始或失败!`,
                browserDownloadUnknownError: `未知错误,有可能是下载时提供的参数存在问题,请检查文件名是否合法!`,
                browserDownloadTimeout: `下载超时,请检查网络环境是否正常!`,
                loadingCompleted: '加载完成',
                settings: '打开设置',
                configError: '脚本配置中存在错误,请修改。',
                alreadyKnowHowToUse: '我已知晓如何使用!!!',
                notice: [
                    { nodeType: 'br' },
                    '测试版本,发现问题请前往GitHub反馈!'
                ],
                pushTaskSucceed: '推送下载任务成功!',
                connectionTest: '连接测试',
                settingsCheck: '配置检查',
                createTask: '创建任务',
                downloadPathError: '下载路径错误!',
                browserDownloadModeError: '请启用脚本管理器的浏览器API下载模式!',
                parsingProgress: '解析进度: ',
                downloadFailed: '下载失败!',
                downloadThis: '下载当前',
                downloadAll: '下载所有',
                allCompleted: '全部完成!',
                pushTaskFailed: '推送下载任务失败!'
            };
        }
    }
    class Config {
        constructor() {
            this.language = language();
            this.downloadType = DownloadType.Others;
            this.downloadPath = '/IwaraZip/%#FileName#%';
            this.downloadProxy = '';
            this.downloadProxyUser = '';
            this.downloadProxyPassword = '';
            this.aria2Path = 'http://127.0.0.1:6800/jsonrpc';
            this.aria2Token = '';
            let body = new Proxy(this, {
                get: function (target, property) {
                    if (property === 'configChange') {
                        return target.configChange;
                    }
                    let value = GM_getValue(property, target[property]);
                    GM_getValue('isDebug') && console.log(`get: ${property} ${getString(value)}`);
                    return value;
                },
                set: function (target, property, value) {
                    if (property === 'configChange') {
                        target.configChange = value;
                        return true;
                    }
                    GM_setValue(property, value);
                    GM_getValue('isDebug') && console.log(`set: ${property} ${getString(value)}`);
                    target.configChange(property);
                    return true;
                }
            });
            GM_listValues().forEach((value) => {
                GM_addValueChangeListener(value, (name, old_value, new_value, remote) => {
                    GM_getValue('isDebug') && console.log(`$Is Remote: ${remote} Change Value: ${name}`);
                    if (remote && !isNull(body.configChange))
                        body.configChange(name);
                });
            });
            return body;
        }
        async check() {
            switch (this.downloadType) {
                case DownloadType.Aria2:
                    return await aria2Check();
                case DownloadType.Browser:
                    return await EnvCheck();
                default:
                    break;
            }
            return true;
        }
    }
    class configEdit {
        constructor(config) {
            this.target = config;
            this.target.configChange = (item) => { this.configChange.call(this, item); };
            this.interfacePage = renderNode({
                nodeType: 'p'
            });
            let save = renderNode({
                nodeType: 'button',
                childs: '%#save#%',
                attributes: {
                    title: i18n[language()].save
                },
                events: {
                    click: async () => {
                        save.disabled = !save.disabled;
                        if (await this.target.check()) {
                            unsafeWindow.location.reload();
                        }
                        save.disabled = !save.disabled;
                    }
                }
            });
            let reset = renderNode({
                nodeType: 'button',
                childs: '%#reset#%',
                attributes: {
                    title: i18n[language()].reset
                },
                events: {
                    click: () => {
                        firstRun();
                        unsafeWindow.location.reload();
                    }
                }
            });
            this.interface = renderNode({
                nodeType: 'div',
                attributes: {
                    id: 'pluginConfig'
                },
                childs: [
                    {
                        nodeType: 'div',
                        className: 'main',
                        childs: [
                            {
                                nodeType: 'h2',
                                childs: '%#appName#%'
                            },
                            {
                                nodeType: 'label',
                                childs: [
                                    '%#language#% ',
                                    {
                                        nodeType: 'input',
                                        className: 'inputRadioLine',
                                        attributes: Object.assign({
                                            name: 'language',
                                            type: 'text',
                                            value: this.target.language
                                        }),
                                        events: {
                                            change: (event) => {
                                                this.target.language = event.target.value;
                                            }
                                        }
                                    }
                                ]
                            },
                            this.downloadTypeSelect(),
                            this.interfacePage,
                            this.switchButton('isDebug', GM_getValue, (name, e) => { GM_setValue(name, e.target.checked); }, false),
                        ]
                    },
                    {
                        nodeType: 'p',
                        className: 'buttonList',
                        childs: [
                            reset,
                            save
                        ]
                    }
                ]
            });
        }
        switchButton(name, get, set, defaultValue) {
            let button = renderNode({
                nodeType: 'p',
                className: 'inputRadioLine',
                childs: [
                    {
                        nodeType: 'label',
                        childs: `%#${name}#%`,
                        attributes: {
                            for: name
                        }
                    }, {
                        nodeType: 'input',
                        className: 'switch',
                        attributes: {
                            type: 'checkbox',
                            name: name,
                        },
                        events: {
                            change: (e) => {
                                if (set !== undefined) {
                                    set(name, e);
                                    return;
                                }
                                else {
                                    this.target[name] = e.target.checked;
                                }
                            }
                        }
                    }
                ]
            });
            return button;
        }
        inputComponent(name, type, get, set) {
            return {
                nodeType: 'label',
                childs: [
                    `%#${name}#% `,
                    {
                        nodeType: 'input',
                        attributes: Object.assign({
                            name: name,
                            type: type ?? 'text',
                            value: get !== undefined ? get(name) : this.target[name]
                        }),
                        events: {
                            change: (e) => {
                                if (set !== undefined) {
                                    set(name, e);
                                    return;
                                }
                                else {
                                    this.target[name] = e.target.value;
                                }
                            }
                        }
                    }
                ]
            };
        }
        downloadTypeSelect() {
            let select = renderNode({
                nodeType: 'p',
                className: 'inputRadioLine',
                childs: [
                    `%#downloadType#%`,
                    {
                        nodeType: 'select',
                        childs: Object.keys(DownloadType).filter((i) => isNaN(Number(i))).map((i) => renderNode({
                            nodeType: 'option',
                            childs: i
                        })),
                        attributes: {
                            name: 'downloadType'
                        },
                        events: {
                            change: (e) => {
                                this.target.downloadType = e.target.selectedIndex;
                            }
                        }
                    }
                ]
            });
            select.selectedIndex = Number(this.target.downloadType);
            return select;
        }
        configChange(item) {
            switch (item) {
                case 'downloadType':
                    this.interface.querySelector(`[name=${item}]`).selectedIndex = Number(this.target.downloadType);
                    this.pageChange();
                    break;
                case 'checkPriority':
                    this.pageChange();
                    break;
                default:
                    let element = this.interface.querySelector(`[name=${item}]`);
                    if (element) {
                        switch (element.type) {
                            case 'radio':
                                element.value = this.target[item];
                                break;
                            case 'checkbox':
                                element.checked = this.target[item];
                                break;
                            case 'text':
                            case 'password':
                                element.value = this.target[item];
                                break;
                            default:
                                break;
                        }
                    }
                    break;
            }
        }
        pageChange() {
            while (this.interfacePage.hasChildNodes()) {
                this.interfacePage.removeChild(this.interfacePage.firstChild);
            }
            let downloadConfigInput = [
                renderNode(this.inputComponent('downloadPath')),
                renderNode(this.inputComponent('downloadProxy')),
                renderNode(this.inputComponent('downloadProxyUser')),
                renderNode(this.inputComponent('downloadProxyPassword')),
            ];
            let aria2ConfigInput = [
                renderNode(this.inputComponent('aria2Path')),
                renderNode(this.inputComponent('aria2Token', 'password'))
            ];
            let BrowserConfigInput = [
                renderNode(this.inputComponent('downloadPath'))
            ];
            switch (this.target.downloadType) {
                case DownloadType.Aria2:
                    downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    aria2ConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
                default:
                    BrowserConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
            }
            if (this.target.checkPriority) {
                originalNodeAppendChild.call(this.interfacePage, renderNode(this.inputComponent('downloadPriority')));
            }
        }
        inject() {
            if (!unsafeWindow.document.querySelector('#pluginConfig')) {
                originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
                this.configChange('downloadType');
            }
        }
    }
    class menu {
        constructor() {
            this.interfacePage = renderNode({
                nodeType: 'ul'
            });
            this.interface = renderNode({
                nodeType: 'div',
                attributes: {
                    id: 'pluginMenu'
                },
                childs: this.interfacePage
            });
        }
        button(name, click) {
            return renderNode(prune({
                nodeType: 'li',
                childs: `%#${name}#%`,
                events: {
                    click: (event) => {
                        click(name, event);
                        event.stopPropagation();
                        return false;
                    }
                }
            }));
        }
        inject() {
            if (!unsafeWindow.document.querySelector('#pluginMenu')) {
                let downloadThisButton = this.button('downloadThis', async (name, event) => {
                    let title = unsafeWindow.document.querySelector('.image-name-title');
                    let downloadButton = unsafeWindow.document.querySelector('button[onclick^="openUrl"]');
                    pushDownloadTask(new FileInfo(title.innerText, downloadButton.getAttribute('onclick').among("openUrl('", "');", true)));
                });
                let downloadAllButton = this.button('downloadAll', (name, event) => {
                    manageDownloadTaskQueue();
                });
                let settingsButton = this.button('settings', (name, event) => {
                    editConfig.inject();
                });
                originalNodeAppendChild.call(this.interfacePage, downloadAllButton);
                originalNodeAppendChild.call(this.interfacePage, downloadThisButton);
                originalNodeAppendChild.call(this.interfacePage, settingsButton);
                originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
            }
        }
    }
    class FileInfo {
        constructor(name, url) {
            if (!isNull(name) || !isNull(url)) {
                this.url = url.toURL();
                this.name = name;
                this.isInit = true;
            }
        }
        async init(element) {
            if (!isNull(element)) {
                this.url = new URL(`${element.getAttribute('dtfullurl')}/${element.getAttribute('dtsafefilenameforurl')}`);
                this.name = element.getAttribute('dtfilename');
                this.fileID = element.getAttribute('fileid');
                let details = await (await fetch("https://www.iwara.zip/account/ajax/file_details", {
                    "headers": {
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    "referrer": "https://www.iwara.zip/",
                    "body": `u=${this.fileID}&p=true`,
                    "method": "POST"
                })).json();
                this.token = details.html.among('download_token=', '\'');
                this.url.searchParams.append("download_token", this.token);
            }
            return this;
        }
    }
    GM_addStyle(GM_getResourceText('toastify-css'));
    GM_addStyle(`

        :root {
            --body: #f2f2f2;
            --body-alt: #ededed;
            --body-dark: #e8e8e8;
            --body-darker: #dedede;
            --text: #444;
            --muted: #848484;
            --error: #be5046;
            --error-text: #f8f8ff;
            --danger: #be5046;
            --danger-dark: #7b1a11;
            --danger-text: #f8f8ff;
            --warning: #dda82b;
            --warning-dark: #dda82b;
            --warning-text: white;
            --success: #45aa63;
            --wura: #dda82b;
            --primary: #1abc9c;
            --primary-text: #f8f8ff;
            --primary-dark: #19b898;
            --primary-faded: rgba(26, 188, 156, 0.2);
            --secondary: #ff004b;
            --secondary-dark: #eb0045;
            --white: #f8f8ff;
            --red: #c64a4a;
            --green: green;
            --yellow: yellow;
            --blue: blue;
            --admin-color: #d98350;
            --moderator-color: #9889ff;
            --premium-color: #ff62cd;
            color: var(--text)
        }

        .rainbow-text {
            background-image: linear-gradient(to right, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-size: 600% 100%;
            animation: rainbow 0.5s infinite linear;
        }
        @keyframes rainbow {
            0% {
                background-position: 0% 0%;
            }
            100% {
                background-position: 100% 0%;
            }
        }

        #pluginMenu {
            z-index: 2147483644;
            color: white;
            position: fixed;
            top: 50%;
            right: 0px;
            padding: 10px;
            background-color: #565656;
            border: 1px solid #ccc;
            border-radius: 5px;
            box-shadow: 0 0 10px #ccc;
            transform: translate(2%, -50%);
        }
        #pluginMenu ul {
            list-style: none;
            margin: 0;
            padding: 0;
        }
        #pluginMenu li {
            padding: 5px 10px;
            cursor: pointer;
            text-align: center;
            user-select: none;
        }
        #pluginMenu li:hover {
            background-color: #000000cc;
            border-radius: 3px;
        }

        #pluginConfig {
            color: var(--text);
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.75);
            z-index: 2147483646; 
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
        #pluginConfig .main {
            background-color: var(--body);
            padding: 24px;
            margin: 10px;
            overflow-y: auto;
            width: 400px;
        }
        #pluginConfig .buttonList {
            display: flex;
            flex-direction: row;
            justify-content: center;
        }
        @media (max-width: 640px) {
            #pluginConfig .main {
                width: 100%;
            }
        }
        #pluginConfig button {
            background-color: blue;
            margin: 0px 20px 0px 20px;
            padding: 10px 20px;
            color: white;
            font-size: 18px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        #pluginConfig button {
            background-color: blue;
        }
        #pluginConfig button[disabled] {
            background-color: darkgray;
            cursor: not-allowed;
        }
        #pluginConfig p {
            display: flex;
            flex-direction: column;
        }
        #pluginConfig p label{
            display: flex;
            flex-direction: column;
            margin: 5px 0 5px 0;
        }
        #pluginConfig .inputRadioLine {
            display: flex;
            align-items: center;
            flex-direction: row;
            justify-content: space-between;
        }
        #pluginConfig input[type="text"], #pluginConfig input[type="password"] {
            outline: none;
            border-top: none;
            border-right: none;
            border-left: none;
            border-image: initial;
            border-bottom: 1px solid var(--muted);
            line-height: 1;
            height: 30px;
            box-sizing: border-box;
            width: 100%;
            background-color: var(--body);
            color: var(--text);
        }
        #pluginConfig input[type='checkbox'].switch{
            outline: none;
            appearance: none;
            -webkit-appearance: none;
            -moz-appearance: none;
            position: relative;
            width: 40px;
            height: 20px;
            background: #ccc;
            border-radius: 10px;
            transition: border-color .2s, background-color .2s;
        }
        #pluginConfig input[type='checkbox'].switch::after {
            content: '';
            display: inline-block;
            width: 40%;
            height: 80%;
            border-radius: 50%;
            background: #fff;
            box-shadow: 0, 0, 2px, #999;
            transition: .2s;
            top: 2px;
            position: absolute;
            right: 55%;
        }
        #pluginConfig input[type='checkbox'].switch:checked {
            background: rgb(19, 206, 102);
        }
        #pluginConfig input[type='checkbox'].switch:checked::after {
            content: '';
            position: absolute;
            right: 2px;
            top: 2px;
        }

        #pluginOverlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.75);
            z-index: 2147483645; 
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
        #pluginOverlay .main {
            color: white;
            font-size: 24px;
            width: 60%;
            background-color: rgba(64, 64, 64, 0.75);
            padding: 24px;
            margin: 10px;
            overflow-y: auto;
        }
        @media (max-width: 640px) {
            #pluginOverlay .main {
                width: 100%;
            }
        }
        #pluginOverlay button {
            padding: 10px 20px;
            color: white;
            font-size: 18px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        #pluginOverlay button {
            background-color: blue;
        }
        #pluginOverlay button[disabled] {
            background-color: darkgray;
            cursor: not-allowed;
        }
        #pluginOverlay .checkbox {
            width: 32px;
            height: 32px;
            margin: 0 4px 0 0;
            padding: 0;
        }
        #pluginOverlay .checkbox-container {
            display: flex;
            align-items: center;
            margin: 0 0 10px 0;
        }
        #pluginOverlay .checkbox-label {
            color: white;
            font-size: 32px;
            font-weight: bold;
            margin-left: 10px;
            display: flex;
            align-items: center;
        }

        .toastify h3 {
            margin: 0 0 10px 0;
        }
        .toastify p {
            margin: 0 ;
        }
    `);
    var i18n = new I18N();
    var config = new Config();
    var editConfig = new configEdit(config);
    var pluginMenu = new menu();
    function toastNode(body, title) {
        return renderNode({
            nodeType: 'div',
            childs: [
                !isNull(title) && !title.isEmpty() ? {
                    nodeType: 'h3',
                    childs: `%#appName#% - ${title}`
                } : {
                    nodeType: 'h3',
                    childs: '%#appName#%'
                },
                {
                    nodeType: 'p',
                    childs: body
                }
            ]
        });
    }
    function getTextNode(node) {
        return node.nodeType === Node.TEXT_NODE
            ? node.textContent || ''
            : node.nodeType === Node.ELEMENT_NODE
                ? Array.from(node.childNodes)
                    .map(getTextNode)
                    .join('')
                : '';
    }
    function newToast(type, params) {
        const logFunc = {
            [ToastType.Warn]: console.warn,
            [ToastType.Error]: console.error,
            [ToastType.Log]: console.log,
            [ToastType.Info]: console.info,
        }[type] || console.log;
        params = Object.assign({
            newWindow: true,
            gravity: 'top',
            position: 'left',
            stopOnFocus: true
        }, type === ToastType.Warn && {
            duration: -1,
            style: {
                background: 'linear-gradient(-30deg, rgb(119 76 0), rgb(255 165 0))'
            }
        }, type === ToastType.Error && {
            duration: -1,
            style: {
                background: 'linear-gradient(-30deg, rgb(108 0 0), rgb(215 0 0))'
            }
        }, !isNull(params) && params);
        if (!isNull(params.text)) {
            params.text = params.text.replaceVariable(i18n[language()]).toString();
        }
        logFunc((!isNull(params.text) ? params.text : !isNull(params.node) ? getTextNode(params.node) : 'undefined').replaceVariable(i18n[language()]));
        return Toastify(params);
    }
    function analyzeLocalPath(path) {
        let matchPath = path.replaceAll('//', '/').replaceAll('\\\\', '/').match(/^([a-zA-Z]:)?[\/\\]?([^\/\\]+[\/\\])*([^\/\\]+\.\w+)$/);
        if (isNull(matchPath))
            throw new Error(`%#downloadPathError#%["${path}"]`);
        try {
            return {
                fullPath: matchPath[0],
                drive: matchPath[1] || '',
                filename: matchPath[3]
            };
        }
        catch (error) {
            throw new Error(`%#downloadPathError#% ["${matchPath.join(',')}"]`);
        }
    }
    function pushDownloadTask(fileInfo) {
        switch (config.downloadType) {
            case DownloadType.Aria2:
                aria2Download(fileInfo);
                break;
            case DownloadType.Browser:
                browserDownload(fileInfo);
                break;
            default:
                othersDownload(fileInfo);
                break;
        }
    }
    async function manageDownloadTaskQueue() {
        let list = document.querySelectorAll('div[fileid]');
        let size = list.length;
        let node = renderNode({
            nodeType: 'p',
            childs: `%#parsingProgress#%[${list.length}/${size}]`
        });
        let start = newToast(ToastType.Info, {
            node: node,
            duration: -1
        });
        start.showToast();
        for (let index = 0; index < list.length; index++) {
            pushDownloadTask(await (new FileInfo()).init(list[index]));
            node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.length - (index + 1)}/${size}]`;
        }
        start.hideToast();
        if (size != 1) {
            let completed = newToast(ToastType.Info, {
                text: `%#allCompleted#%`,
                duration: -1,
                close: true,
                onClick() {
                    completed.hideToast();
                }
            });
            completed.showToast();
        }
    }
    function aria2Download(fileInfo) {
        (async function (name, downloadUrl) {
            let localPath = analyzeLocalPath(config.downloadPath.replaceVariable({
                FileName: name
            }).trim());
            let res = await aria2API('aria2.addUri', [
                [downloadUrl.href],
                prune({
                    'all-proxy': config.downloadProxy,
                    'all-proxy-user': config.downloadProxyUser,
                    'all-proxy-passwd': config.downloadProxyPassword,
                    'out': localPath.filename,
                    'dir': localPath.fullPath.replace(localPath.filename, ''),
                    'referer': window.location.hostname,
                    'header': [
                        'Cookie:' + unsafeWindow.document.cookie
                    ]
                })
            ]);
            console.log(`Aria2 ${name} ${JSON.stringify(res)}`);
            newToast(ToastType.Info, {
                node: toastNode(`${name} %#pushTaskSucceed#%`)
            }).showToast();
        }(fileInfo.name, fileInfo.url));
    }
    function othersDownload(fileInfo) {
        (async function (Name, DownloadUrl) {
            DownloadUrl.searchParams.set('download', analyzeLocalPath(config.downloadPath.replaceVariable({
                FileName: Name
            }).trim()).filename);
            GM_openInTab(DownloadUrl.href, { active: false, insert: true, setParent: true });
        }(fileInfo.name, fileInfo.url));
    }
    function browserDownload(fileInfo) {
        (async function (Name, DownloadUrl) {
            function browserDownloadError(error) {
                let errorInfo = getString(Error);
                if (!(error instanceof Error)) {
                    errorInfo = {
                        'not_enabled': `%#browserDownloadNotEnabled#%`,
                        'not_whitelisted': `%#browserDownloadNotWhitelisted#%`,
                        'not_permitted': `%#browserDownloadNotPermitted#%`,
                        'not_supported': `%#browserDownloadNotSupported#%`,
                        'not_succeeded': `%#browserDownloadNotSucceeded#% ${error.details ?? getString(error.details)}`
                    }[error.error] || `%#browserDownloadUnknownError#%`;
                }
                let toast = newToast(ToastType.Error, {
                    node: toastNode([
                        `${Name} %#downloadFailed#%`,
                        { nodeType: 'br' },
                        errorInfo,
                        { nodeType: 'br' },
                        `%#tryRestartingDownload#%`
                    ], '%#browserDownload#%'),
                    async onClick() {
                        toast.hideToast();
                    }
                });
                toast.showToast();
            }
            GM_download({
                url: DownloadUrl.href,
                saveAs: false,
                name: config.downloadPath.replaceVariable({
                    NowTime: new Date(),
                    FileName: Name
                }).trim(),
                onerror: (err) => browserDownloadError(err),
                ontimeout: () => browserDownloadError(new Error('%#browserDownloadTimeout#%'))
            });
        }(fileInfo.name, fileInfo.url));
    }
    async function aria2API(method, params) {
        return await (await fetch(config.aria2Path, {
            headers: {
                'accept': 'application/json',
                'content-type': 'application/json'
            },
            body: JSON.stringify({
                jsonrpc: '2.0',
                method: method,
                id: UUID(),
                params: [`token:${config.aria2Token}`, ...params]
            }),
            method: 'POST'
        })).json();
    }
    async function EnvCheck() {
        try {
            if (GM_info.downloadMode !== 'browser') {
                GM_getValue('isDebug') && console.log(GM_info);
                throw new Error('%#browserDownloadModeError#%');
            }
        }
        catch (error) {
            let toast = newToast(ToastType.Error, {
                node: toastNode([
                    `%#configError#%`,
                    { nodeType: 'br' },
                    getString(error)
                ], '%#settingsCheck#%'),
                position: 'center',
                onClick() {
                    toast.hideToast();
                }
            });
            toast.showToast();
            return false;
        }
        return true;
    }
    async function aria2Check() {
        try {
            let res = await (await fetch(config.aria2Path, {
                method: 'POST',
                headers: {
                    'accept': 'application/json',
                    'content-type': 'application/json'
                },
                body: JSON.stringify({
                    'jsonrpc': '2.0',
                    'method': 'aria2.tellActive',
                    'id': UUID(),
                    'params': ['token:' + config.aria2Token]
                })
            })).json();
            if (res.error) {
                throw new Error(res.error.message);
            }
        }
        catch (error) {
            let toast = newToast(ToastType.Error, {
                node: toastNode([
                    `Aria2 RPC %#connectionTest#%`,
                    { nodeType: 'br' },
                    getString(error)
                ], '%#settingsCheck#%'),
                position: 'center',
                onClick() {
                    toast.hideToast();
                }
            });
            toast.showToast();
            return false;
        }
        return true;
    }
    function firstRun() {
        console.log('First run config reset!');
        GM_listValues().forEach(i => GM_deleteValue(i));
        config = new Config();
        editConfig = new configEdit(config);
        let confirmButton = renderNode({
            nodeType: 'button',
            attributes: {
                disabled: true,
                title: i18n[language()].ok
            },
            childs: '%#ok#%',
            events: {
                click: () => {
                    GM_setValue('isFirstRun', false);
                    GM_setValue('version', GM_info.script.version);
                    unsafeWindow.document.querySelector('#pluginOverlay').remove();
                    editConfig.inject();
                }
            }
        });
        originalNodeAppendChild.call(unsafeWindow.document.body, renderNode({
            nodeType: 'div',
            attributes: {
                id: 'pluginOverlay'
            },
            childs: [
                {
                    nodeType: 'div',
                    className: 'main',
                    childs: [
                        { nodeType: 'p', childs: '%#useHelpForBase#%' }
                    ]
                },
                {
                    nodeType: 'div',
                    className: 'checkbox-container',
                    childs: {
                        nodeType: 'label',
                        className: ['checkbox-label', 'rainbow-text'],
                        childs: [{
                                nodeType: 'input',
                                className: 'checkbox',
                                attributes: {
                                    type: 'checkbox',
                                    name: 'agree-checkbox'
                                },
                                events: {
                                    change: (event) => {
                                        confirmButton.disabled = !event.target.checked;
                                    }
                                }
                            }, '%#alreadyKnowHowToUse#%']
                    }
                },
                confirmButton
            ]
        }));
    }
    async function main() {
        if (GM_getValue('isFirstRun', true)) {
            firstRun();
            return;
        }
        if (!await config.check()) {
            newToast(ToastType.Info, {
                text: `%#configError#%`,
                duration: 60 * 1000,
            }).showToast();
            editConfig.inject();
            return;
        }
        GM_setValue('version', GM_info.script.version);
        pluginMenu.inject();
        let notice = newToast(ToastType.Info, {
            node: toastNode([
                `加载完成`,
                { nodeType: 'br' },
                `公告: `,
                ...i18n[language()].notice
            ]),
            duration: 10000,
            gravity: 'bottom',
            position: 'center',
            onClick() {
                notice.hideToast();
            }
        });
        notice.showToast();
    }
    if (new Version(GM_getValue('version', '0.0.0')).compare(new Version('0.0.1')) === VersionState.Low) {
        GM_setValue('isFirstRun', true);
        alert(i18n[language()].configurationIncompatible);
    }
    (unsafeWindow.document.body ? Promise.resolve() : new Promise(resolve => originalAddEventListener.call(unsafeWindow.document, "DOMContentLoaded", resolve))).then(main);
})();