您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增强 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/[email protected]/src/toastify.min.js // @resource toastify-css https://cdn.jsdelivr.net/npm/[email protected]/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); })();