// ==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);
})();