IwaraZip Enhancement

Enhancement IwaraZip

  1. // ==UserScript==
  2. // @name IwaraZip Enhancement
  3. // @description Enhancement IwaraZip
  4. // @name:zh-CN IwaraZip 增强
  5. // @description:zh-CN 增强 IwaraZip 使用体验
  6. // @icon https://www.iwara.zip/themes/spirit/assets/images/favicon/favicon.ico
  7. // @namespace https://github.com/dawn-lc/
  8. // @author dawn-lc
  9. // @license Apache-2.0
  10. // @copyright 2024, Dawnlc (https://dawnlc.me/)
  11. // @source https://github.com/dawn-lc/IwaraZipEnhancement
  12. // @supportURL https://github.com/dawn-lc/IwaraZipEnhancement/issues
  13. // @connect iwara.zip
  14. // @connect *.iwara.zip
  15. // @connect localhost
  16. // @connect 127.0.0.1
  17. // @connect *
  18. // @match *://*.iwara.zip/*
  19. // @grant GM_getValue
  20. // @grant GM_setValue
  21. // @grant GM_listValues
  22. // @grant GM_deleteValue
  23. // @grant GM_addValueChangeListener
  24. // @grant GM_addStyle
  25. // @grant GM_addElement
  26. // @grant GM_getResourceText
  27. // @grant GM_setClipboard
  28. // @grant GM_download
  29. // @grant GM_xmlhttpRequest
  30. // @grant GM_openInTab
  31. // @grant GM_info
  32. // @grant unsafeWindow
  33. // @run-at document-start
  34. // @require https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js
  35. // @resource toastify-css https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.css
  36. // @version 0.1.15
  37. // ==/UserScript==
  38. (function () {
  39. const originalFetch = unsafeWindow.fetch;
  40. const originalNodeAppendChild = unsafeWindow.Node.prototype.appendChild;
  41. const originalAddEventListener = unsafeWindow.EventTarget.prototype.addEventListener;
  42. const isNull = (obj) => typeof obj === 'undefined' || obj === null;
  43. const isObject = (obj) => !isNull(obj) && typeof obj === 'object' && !Array.isArray(obj);
  44. const isString = (obj) => !isNull(obj) && typeof obj === 'string';
  45. const isNumber = (obj) => !isNull(obj) && typeof obj === 'number';
  46. const isElement = (obj) => !isNull(obj) && obj instanceof Element;
  47. const isNode = (obj) => !isNull(obj) && obj instanceof Node;
  48. const isStringTupleArray = (obj) => Array.isArray(obj) && obj.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string');
  49. const hasFunction = (obj, method) => {
  50. return !method.isEmpty() && !isNull(obj) ? method in obj && typeof obj[method] === 'function' : false;
  51. };
  52. const getString = (obj) => {
  53. obj = obj instanceof Error ? String(obj) : obj;
  54. obj = obj instanceof Date ? obj.format('YYYY-MM-DD') : obj;
  55. return typeof obj === 'object' ? JSON.stringify(obj, null, 2) : String(obj);
  56. };
  57. Array.prototype.any = function () {
  58. return this.prune().length > 0;
  59. };
  60. Array.prototype.prune = function () {
  61. return this.filter(i => i !== null && typeof i !== 'undefined');
  62. };
  63. Array.prototype.unique = function (prop) {
  64. return this.filter((item, index, self) => index === self.findIndex((t) => (prop ? t[prop] === item[prop] : t === item)));
  65. };
  66. Array.prototype.union = function (that, prop) {
  67. return [...this, ...that].unique(prop);
  68. };
  69. Array.prototype.intersect = function (that, prop) {
  70. return this.filter((item) => that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
  71. };
  72. Array.prototype.difference = function (that, prop) {
  73. return this.filter((item) => !that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
  74. };
  75. Array.prototype.complement = function (that, prop) {
  76. return this.union(that, prop).difference(this.intersect(that, prop), prop);
  77. };
  78. String.prototype.isEmpty = function () {
  79. return !isNull(this) && this.length === 0;
  80. };
  81. String.prototype.among = function (start, end, greedy = false) {
  82. if (this.isEmpty() || start.isEmpty() || end.isEmpty())
  83. return '';
  84. const startIndex = this.indexOf(start);
  85. if (startIndex === -1)
  86. return '';
  87. const adjustedStartIndex = startIndex + start.length;
  88. const endIndex = greedy ? this.lastIndexOf(end) : this.indexOf(end, adjustedStartIndex);
  89. if (endIndex === -1 || endIndex < adjustedStartIndex)
  90. return '';
  91. return this.slice(adjustedStartIndex, endIndex);
  92. };
  93. String.prototype.splitLimit = function (separator, limit) {
  94. if (this.isEmpty() || isNull(separator)) {
  95. throw new Error('Empty');
  96. }
  97. let body = this.split(separator);
  98. return limit ? body.slice(0, limit).concat(body.slice(limit).join(separator)) : body;
  99. };
  100. String.prototype.truncate = function (maxLength) {
  101. return this.length > maxLength ? this.substring(0, maxLength) : this.toString();
  102. };
  103. String.prototype.trimHead = function (prefix) {
  104. return this.startsWith(prefix) ? this.slice(prefix.length) : this.toString();
  105. };
  106. String.prototype.trimTail = function (suffix) {
  107. return this.endsWith(suffix) ? this.slice(0, -suffix.length) : this.toString();
  108. };
  109. String.prototype.toURL = function () {
  110. let URLString = this;
  111. if (URLString.split('//')[0].isEmpty()) {
  112. URLString = `${unsafeWindow.location.protocol}${URLString}`;
  113. }
  114. return new URL(URLString.toString());
  115. };
  116. Array.prototype.append = function (arr) {
  117. this.push(...arr);
  118. };
  119. String.prototype.replaceVariable = function (replacements, count = 0) {
  120. let replaceString = this.toString();
  121. try {
  122. replaceString = Object.entries(replacements).reduce((str, [key, value]) => {
  123. if (str.includes(`%#${key}:`)) {
  124. let format = str.among(`%#${key}:`, '#%').toString();
  125. return str.replaceAll(`%#${key}:${format}#%`, getString(hasFunction(value, 'format') ? value.format(format) : value));
  126. }
  127. else {
  128. return str.replaceAll(`%#${key}#%`, getString(value));
  129. }
  130. }, replaceString);
  131. count++;
  132. return Object.keys(replacements).map((key) => this.includes(`%#${key}#%`)).includes(true) && count < 128 ? replaceString.replaceVariable(replacements, count) : replaceString;
  133. }
  134. catch (error) {
  135. GM_getValue('isDebug') && console.log(`replace variable error: ${getString(error)}`);
  136. return replaceString;
  137. }
  138. };
  139. function prune(obj) {
  140. if (Array.isArray(obj)) {
  141. return obj.filter(isNotEmpty).map(prune);
  142. }
  143. if (isElement(obj) || isNode(obj)) {
  144. return obj;
  145. }
  146. if (isObject(obj)) {
  147. return Object.fromEntries(Object.entries(obj)
  148. .filter(([key, value]) => isNotEmpty(value))
  149. .map(([key, value]) => [key, prune(value)]));
  150. }
  151. return isNotEmpty(obj) ? obj : undefined;
  152. }
  153. function isNotEmpty(obj) {
  154. if (isNull(obj)) {
  155. return false;
  156. }
  157. if (Array.isArray(obj)) {
  158. return obj.some(isNotEmpty);
  159. }
  160. if (isString(obj)) {
  161. return !obj.isEmpty();
  162. }
  163. if (isNumber(obj)) {
  164. return !Number.isNaN(obj);
  165. }
  166. if (isElement(obj) || isNode(obj)) {
  167. return true;
  168. }
  169. if (isObject(obj)) {
  170. return Object.values(obj).some(isNotEmpty);
  171. }
  172. return true;
  173. }
  174. const fetch = (input, init, force) => {
  175. if (init && init.headers && isStringTupleArray(init.headers))
  176. throw new Error("init headers Error");
  177. if (init && init.method && !(init.method === 'GET' || init.method === 'HEAD' || init.method === 'POST'))
  178. throw new Error("init method Error");
  179. return force || (typeof input === 'string' ? input : input.url).toURL().hostname !== unsafeWindow.location.hostname ? new Promise((resolve, reject) => {
  180. GM_xmlhttpRequest(prune({
  181. method: (init && init.method) || 'GET',
  182. url: typeof input === 'string' ? input : input.url,
  183. headers: (init && init.headers) || {},
  184. data: ((init && init.body) || null),
  185. onload: function (response) {
  186. resolve(new Response(response.responseText, {
  187. status: response.status,
  188. statusText: response.statusText,
  189. }));
  190. },
  191. onerror: function (error) {
  192. reject(error);
  193. }
  194. }));
  195. }) : originalFetch(input, init);
  196. };
  197. const UUID = function () {
  198. return Array.from({ length: 8 }, () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)).join('');
  199. };
  200. const ceilDiv = function (dividend, divisor) {
  201. return Math.floor(dividend / divisor) + (dividend % divisor > 0 ? 1 : 0);
  202. };
  203. const language = function () {
  204. let env = (!isNull(config) ? config.language : (navigator.language ?? navigator.languages[0] ?? 'en')).replace('-', '_');
  205. let main = env.split('_').shift() ?? 'en';
  206. return (!isNull(i18n[env]) ? env : !isNull(i18n[main]) ? main : 'en');
  207. };
  208. const renderNode = function (renderCode) {
  209. renderCode = prune(renderCode);
  210. if (isNull(renderCode))
  211. throw new Error("RenderCode null");
  212. if (typeof renderCode === 'string') {
  213. return document.createTextNode(renderCode.replaceVariable(i18n[language()]).toString());
  214. }
  215. if (renderCode instanceof Node) {
  216. return renderCode;
  217. }
  218. if (typeof renderCode !== 'object' || !renderCode.nodeType) {
  219. throw new Error('Invalid arguments');
  220. }
  221. const { nodeType, attributes, events, className, childs } = renderCode;
  222. const node = document.createElement(nodeType);
  223. (!isNull(attributes) && Object.keys(attributes).any()) && Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, value));
  224. (!isNull(events) && Object.keys(events).any()) && Object.entries(events).forEach(([eventName, eventHandler]) => originalAddEventListener.call(node, eventName, eventHandler));
  225. (!isNull(className) && className.length > 0) && node.classList.add(...[].concat(className));
  226. !isNull(childs) && node.append(...[].concat(childs).map(renderNode));
  227. return node;
  228. };
  229. let ToastType;
  230. (function (ToastType) {
  231. ToastType[ToastType["Log"] = 0] = "Log";
  232. ToastType[ToastType["Info"] = 1] = "Info";
  233. ToastType[ToastType["Warn"] = 2] = "Warn";
  234. ToastType[ToastType["Error"] = 3] = "Error";
  235. })(ToastType || (ToastType = {}));
  236. let VersionState;
  237. (function (VersionState) {
  238. VersionState[VersionState["Low"] = 0] = "Low";
  239. VersionState[VersionState["Equal"] = 1] = "Equal";
  240. VersionState[VersionState["High"] = 2] = "High";
  241. })(VersionState || (VersionState = {}));
  242. class Version {
  243. constructor(versionString) {
  244. const [version, preRelease, buildMetadata] = versionString.split(/[-+]/);
  245. const versionParts = version.split('.').map(Number);
  246. this.major = versionParts[0] || 0;
  247. this.minor = versionParts.length > 1 ? versionParts[1] : 0;
  248. this.patch = versionParts.length > 2 ? versionParts[2] : 0;
  249. this.preRelease = preRelease ? preRelease.split('.') : [];
  250. this.buildMetadata = buildMetadata;
  251. }
  252. compare(other) {
  253. const compareSegment = (a, b) => {
  254. if (a < b) {
  255. return VersionState.Low;
  256. }
  257. else if (a > b) {
  258. return VersionState.High;
  259. }
  260. return VersionState.Equal;
  261. };
  262. let state = compareSegment(this.major, other.major);
  263. if (state !== VersionState.Equal)
  264. return state;
  265. state = compareSegment(this.minor, other.minor);
  266. if (state !== VersionState.Equal)
  267. return state;
  268. state = compareSegment(this.patch, other.patch);
  269. if (state !== VersionState.Equal)
  270. return state;
  271. for (let i = 0; i < Math.max(this.preRelease.length, other.preRelease.length); i++) {
  272. const pre1 = this.preRelease[i];
  273. const pre2 = other.preRelease[i];
  274. if (pre1 === undefined && pre2 !== undefined) {
  275. return VersionState.High;
  276. }
  277. else if (pre1 !== undefined && pre2 === undefined) {
  278. return VersionState.Low;
  279. }
  280. if (pre1 !== undefined && pre2 !== undefined) {
  281. state = compareSegment(isNaN(+pre1) ? pre1 : +pre1, isNaN(+pre2) ? pre2 : +pre2);
  282. if (state !== VersionState.Equal)
  283. return state;
  284. }
  285. }
  286. return VersionState.Equal;
  287. }
  288. }
  289. if (GM_getValue('isDebug')) {
  290. console.log(getString(GM_info));
  291. debugger;
  292. }
  293. let DownloadType;
  294. (function (DownloadType) {
  295. DownloadType[DownloadType["Aria2"] = 0] = "Aria2";
  296. DownloadType[DownloadType["Browser"] = 1] = "Browser";
  297. DownloadType[DownloadType["Others"] = 2] = "Others";
  298. })(DownloadType || (DownloadType = {}));
  299. class I18N {
  300. constructor() {
  301. this.zh_CN = this['zh'];
  302. this.zh = {
  303. appName: 'IwaraZip 增强',
  304. language: '语言: ',
  305. downloadPath: '下载到: ',
  306. downloadProxy: '下载代理: ',
  307. downloadProxyUser: '代理用户名: ',
  308. downloadProxyPassword: '下载密码: ',
  309. aria2Path: 'Aria2 RPC: ',
  310. aria2Token: 'Aria2 密钥: ',
  311. rename: '重命名',
  312. save: '保存',
  313. reset: '重置',
  314. ok: '确定',
  315. on: '开启',
  316. off: '关闭',
  317. isDebug: '调试模式',
  318. downloadType: '下载方式',
  319. browserDownload: '浏览器下载',
  320. configurationIncompatible: '检测到不兼容的配置文件,请重新配置!',
  321. browserDownloadNotEnabled: `未启用下载功能!`,
  322. browserDownloadNotWhitelisted: `请求的文件扩展名未列入白名单!`,
  323. browserDownloadNotPermitted: `下载功能已启用,但未授予下载权限!`,
  324. browserDownloadNotSupported: `目前浏览器/版本不支持下载功能!`,
  325. browserDownloadNotSucceeded: `下载未开始或失败!`,
  326. browserDownloadUnknownError: `未知错误,有可能是下载时提供的参数存在问题,请检查文件名是否合法!`,
  327. browserDownloadTimeout: `下载超时,请检查网络环境是否正常!`,
  328. loadingCompleted: '加载完成',
  329. settings: '打开设置',
  330. configError: '脚本配置中存在错误,请修改。',
  331. alreadyKnowHowToUse: '我已知晓如何使用!!!',
  332. notice: [
  333. { nodeType: 'br' },
  334. '测试版本,发现问题请前往GitHub反馈!'
  335. ],
  336. pushTaskSucceed: '推送下载任务成功!',
  337. connectionTest: '连接测试',
  338. settingsCheck: '配置检查',
  339. createTask: '创建任务',
  340. downloadPathError: '下载路径错误!',
  341. browserDownloadModeError: '请启用脚本管理器的浏览器API下载模式!',
  342. parsingProgress: '解析进度: ',
  343. downloadFailed: '下载失败!',
  344. downloadThis: '下载当前',
  345. downloadAll: '下载所有',
  346. allCompleted: '全部完成!',
  347. pushTaskFailed: '推送下载任务失败!'
  348. };
  349. }
  350. }
  351. class Config {
  352. constructor() {
  353. this.language = language();
  354. this.downloadType = DownloadType.Others;
  355. this.downloadPath = '/IwaraZip/%#FileName#%';
  356. this.downloadProxy = '';
  357. this.downloadProxyUser = '';
  358. this.downloadProxyPassword = '';
  359. this.aria2Path = 'http://127.0.0.1:6800/jsonrpc';
  360. this.aria2Token = '';
  361. let body = new Proxy(this, {
  362. get: function (target, property) {
  363. if (property === 'configChange') {
  364. return target.configChange;
  365. }
  366. let value = GM_getValue(property, target[property]);
  367. GM_getValue('isDebug') && console.log(`get: ${property} ${getString(value)}`);
  368. return value;
  369. },
  370. set: function (target, property, value) {
  371. if (property === 'configChange') {
  372. target.configChange = value;
  373. return true;
  374. }
  375. GM_setValue(property, value);
  376. GM_getValue('isDebug') && console.log(`set: ${property} ${getString(value)}`);
  377. target.configChange(property);
  378. return true;
  379. }
  380. });
  381. GM_listValues().forEach((value) => {
  382. GM_addValueChangeListener(value, (name, old_value, new_value, remote) => {
  383. GM_getValue('isDebug') && console.log(`$Is Remote: ${remote} Change Value: ${name}`);
  384. if (remote && !isNull(body.configChange))
  385. body.configChange(name);
  386. });
  387. });
  388. return body;
  389. }
  390. async check() {
  391. switch (this.downloadType) {
  392. case DownloadType.Aria2:
  393. return await aria2Check();
  394. case DownloadType.Browser:
  395. return await EnvCheck();
  396. default:
  397. break;
  398. }
  399. return true;
  400. }
  401. }
  402. class configEdit {
  403. constructor(config) {
  404. this.target = config;
  405. this.target.configChange = (item) => { this.configChange.call(this, item); };
  406. this.interfacePage = renderNode({
  407. nodeType: 'p'
  408. });
  409. let save = renderNode({
  410. nodeType: 'button',
  411. childs: '%#save#%',
  412. attributes: {
  413. title: i18n[language()].save
  414. },
  415. events: {
  416. click: async () => {
  417. save.disabled = !save.disabled;
  418. if (await this.target.check()) {
  419. unsafeWindow.location.reload();
  420. }
  421. save.disabled = !save.disabled;
  422. }
  423. }
  424. });
  425. let reset = renderNode({
  426. nodeType: 'button',
  427. childs: '%#reset#%',
  428. attributes: {
  429. title: i18n[language()].reset
  430. },
  431. events: {
  432. click: () => {
  433. firstRun();
  434. unsafeWindow.location.reload();
  435. }
  436. }
  437. });
  438. this.interface = renderNode({
  439. nodeType: 'div',
  440. attributes: {
  441. id: 'pluginConfig'
  442. },
  443. childs: [
  444. {
  445. nodeType: 'div',
  446. className: 'main',
  447. childs: [
  448. {
  449. nodeType: 'h2',
  450. childs: '%#appName#%'
  451. },
  452. {
  453. nodeType: 'label',
  454. childs: [
  455. '%#language#% ',
  456. {
  457. nodeType: 'input',
  458. className: 'inputRadioLine',
  459. attributes: Object.assign({
  460. name: 'language',
  461. type: 'text',
  462. value: this.target.language
  463. }),
  464. events: {
  465. change: (event) => {
  466. this.target.language = event.target.value;
  467. }
  468. }
  469. }
  470. ]
  471. },
  472. this.downloadTypeSelect(),
  473. this.interfacePage,
  474. this.switchButton('isDebug', GM_getValue, (name, e) => { GM_setValue(name, e.target.checked); }, false),
  475. ]
  476. },
  477. {
  478. nodeType: 'p',
  479. className: 'buttonList',
  480. childs: [
  481. reset,
  482. save
  483. ]
  484. }
  485. ]
  486. });
  487. }
  488. switchButton(name, get, set, defaultValue) {
  489. let button = renderNode({
  490. nodeType: 'p',
  491. className: 'inputRadioLine',
  492. childs: [
  493. {
  494. nodeType: 'label',
  495. childs: `%#${name}#%`,
  496. attributes: {
  497. for: name
  498. }
  499. }, {
  500. nodeType: 'input',
  501. className: 'switch',
  502. attributes: {
  503. type: 'checkbox',
  504. name: name,
  505. },
  506. events: {
  507. change: (e) => {
  508. if (set !== undefined) {
  509. set(name, e);
  510. return;
  511. }
  512. else {
  513. this.target[name] = e.target.checked;
  514. }
  515. }
  516. }
  517. }
  518. ]
  519. });
  520. return button;
  521. }
  522. inputComponent(name, type, get, set) {
  523. return {
  524. nodeType: 'label',
  525. childs: [
  526. `%#${name}#% `,
  527. {
  528. nodeType: 'input',
  529. attributes: Object.assign({
  530. name: name,
  531. type: type ?? 'text',
  532. value: get !== undefined ? get(name) : this.target[name]
  533. }),
  534. events: {
  535. change: (e) => {
  536. if (set !== undefined) {
  537. set(name, e);
  538. return;
  539. }
  540. else {
  541. this.target[name] = e.target.value;
  542. }
  543. }
  544. }
  545. }
  546. ]
  547. };
  548. }
  549. downloadTypeSelect() {
  550. let select = renderNode({
  551. nodeType: 'p',
  552. className: 'inputRadioLine',
  553. childs: [
  554. `%#downloadType#%`,
  555. {
  556. nodeType: 'select',
  557. childs: Object.keys(DownloadType).filter((i) => isNaN(Number(i))).map((i) => renderNode({
  558. nodeType: 'option',
  559. childs: i
  560. })),
  561. attributes: {
  562. name: 'downloadType'
  563. },
  564. events: {
  565. change: (e) => {
  566. this.target.downloadType = e.target.selectedIndex;
  567. }
  568. }
  569. }
  570. ]
  571. });
  572. select.selectedIndex = Number(this.target.downloadType);
  573. return select;
  574. }
  575. configChange(item) {
  576. switch (item) {
  577. case 'downloadType':
  578. this.interface.querySelector(`[name=${item}]`).selectedIndex = Number(this.target.downloadType);
  579. this.pageChange();
  580. break;
  581. case 'checkPriority':
  582. this.pageChange();
  583. break;
  584. default:
  585. let element = this.interface.querySelector(`[name=${item}]`);
  586. if (element) {
  587. switch (element.type) {
  588. case 'radio':
  589. element.value = this.target[item];
  590. break;
  591. case 'checkbox':
  592. element.checked = this.target[item];
  593. break;
  594. case 'text':
  595. case 'password':
  596. element.value = this.target[item];
  597. break;
  598. default:
  599. break;
  600. }
  601. }
  602. break;
  603. }
  604. }
  605. pageChange() {
  606. while (this.interfacePage.hasChildNodes()) {
  607. this.interfacePage.removeChild(this.interfacePage.firstChild);
  608. }
  609. let downloadConfigInput = [
  610. renderNode(this.inputComponent('downloadPath')),
  611. renderNode(this.inputComponent('downloadProxy')),
  612. renderNode(this.inputComponent('downloadProxyUser')),
  613. renderNode(this.inputComponent('downloadProxyPassword')),
  614. ];
  615. let aria2ConfigInput = [
  616. renderNode(this.inputComponent('aria2Path')),
  617. renderNode(this.inputComponent('aria2Token', 'password'))
  618. ];
  619. let BrowserConfigInput = [
  620. renderNode(this.inputComponent('downloadPath'))
  621. ];
  622. switch (this.target.downloadType) {
  623. case DownloadType.Aria2:
  624. downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
  625. aria2ConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
  626. break;
  627. default:
  628. BrowserConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
  629. break;
  630. }
  631. if (this.target.checkPriority) {
  632. originalNodeAppendChild.call(this.interfacePage, renderNode(this.inputComponent('downloadPriority')));
  633. }
  634. }
  635. inject() {
  636. if (!unsafeWindow.document.querySelector('#pluginConfig')) {
  637. originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
  638. this.configChange('downloadType');
  639. }
  640. }
  641. }
  642. class menu {
  643. constructor() {
  644. this.interfacePage = renderNode({
  645. nodeType: 'ul'
  646. });
  647. this.interface = renderNode({
  648. nodeType: 'div',
  649. attributes: {
  650. id: 'pluginMenu'
  651. },
  652. childs: this.interfacePage
  653. });
  654. }
  655. button(name, click) {
  656. return renderNode(prune({
  657. nodeType: 'li',
  658. childs: `%#${name}#%`,
  659. events: {
  660. click: (event) => {
  661. click(name, event);
  662. event.stopPropagation();
  663. return false;
  664. }
  665. }
  666. }));
  667. }
  668. inject() {
  669. if (!unsafeWindow.document.querySelector('#pluginMenu')) {
  670. let downloadThisButton = this.button('downloadThis', async (name, event) => {
  671. let title = unsafeWindow.document.querySelector('.image-name-title');
  672. let downloadButton = unsafeWindow.document.querySelector('button[onclick^="openUrl"]');
  673. pushDownloadTask(new FileInfo(title.innerText, downloadButton.getAttribute('onclick').among("openUrl('", "');", true)));
  674. });
  675. let downloadAllButton = this.button('downloadAll', (name, event) => {
  676. manageDownloadTaskQueue();
  677. });
  678. let settingsButton = this.button('settings', (name, event) => {
  679. editConfig.inject();
  680. });
  681. originalNodeAppendChild.call(this.interfacePage, downloadAllButton);
  682. originalNodeAppendChild.call(this.interfacePage, downloadThisButton);
  683. originalNodeAppendChild.call(this.interfacePage, settingsButton);
  684. originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
  685. }
  686. }
  687. }
  688. class FileInfo {
  689. constructor(name, url) {
  690. if (!isNull(name) || !isNull(url)) {
  691. this.url = url.toURL();
  692. this.name = name;
  693. this.isInit = true;
  694. }
  695. }
  696. async init(element) {
  697. if (!isNull(element)) {
  698. this.url = new URL(`${element.getAttribute('dtfullurl')}/${element.getAttribute('dtsafefilenameforurl')}`);
  699. this.name = element.getAttribute('dtfilename');
  700. this.fileID = element.getAttribute('fileid');
  701. let details = await (await fetch("https://www.iwara.zip/account/ajax/file_details", {
  702. "headers": {
  703. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  704. },
  705. "referrer": "https://www.iwara.zip/",
  706. "body": `u=${this.fileID}&p=true`,
  707. "method": "POST"
  708. })).json();
  709. this.token = details.html.among('download_token=', '\'');
  710. this.url.searchParams.append("download_token", this.token);
  711. }
  712. return this;
  713. }
  714. }
  715. GM_addStyle(GM_getResourceText('toastify-css'));
  716. GM_addStyle(`
  717.  
  718. :root {
  719. --body: #f2f2f2;
  720. --body-alt: #ededed;
  721. --body-dark: #e8e8e8;
  722. --body-darker: #dedede;
  723. --text: #444;
  724. --muted: #848484;
  725. --error: #be5046;
  726. --error-text: #f8f8ff;
  727. --danger: #be5046;
  728. --danger-dark: #7b1a11;
  729. --danger-text: #f8f8ff;
  730. --warning: #dda82b;
  731. --warning-dark: #dda82b;
  732. --warning-text: white;
  733. --success: #45aa63;
  734. --wura: #dda82b;
  735. --primary: #1abc9c;
  736. --primary-text: #f8f8ff;
  737. --primary-dark: #19b898;
  738. --primary-faded: rgba(26, 188, 156, 0.2);
  739. --secondary: #ff004b;
  740. --secondary-dark: #eb0045;
  741. --white: #f8f8ff;
  742. --red: #c64a4a;
  743. --green: green;
  744. --yellow: yellow;
  745. --blue: blue;
  746. --admin-color: #d98350;
  747. --moderator-color: #9889ff;
  748. --premium-color: #ff62cd;
  749. color: var(--text)
  750. }
  751.  
  752. .rainbow-text {
  753. background-image: linear-gradient(to right, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff);
  754. -webkit-background-clip: text;
  755. -webkit-text-fill-color: transparent;
  756. background-size: 600% 100%;
  757. animation: rainbow 0.5s infinite linear;
  758. }
  759. @keyframes rainbow {
  760. 0% {
  761. background-position: 0% 0%;
  762. }
  763. 100% {
  764. background-position: 100% 0%;
  765. }
  766. }
  767.  
  768. #pluginMenu {
  769. z-index: 2147483644;
  770. color: white;
  771. position: fixed;
  772. top: 50%;
  773. right: 0px;
  774. padding: 10px;
  775. background-color: #565656;
  776. border: 1px solid #ccc;
  777. border-radius: 5px;
  778. box-shadow: 0 0 10px #ccc;
  779. transform: translate(2%, -50%);
  780. }
  781. #pluginMenu ul {
  782. list-style: none;
  783. margin: 0;
  784. padding: 0;
  785. }
  786. #pluginMenu li {
  787. padding: 5px 10px;
  788. cursor: pointer;
  789. text-align: center;
  790. user-select: none;
  791. }
  792. #pluginMenu li:hover {
  793. background-color: #000000cc;
  794. border-radius: 3px;
  795. }
  796.  
  797. #pluginConfig {
  798. color: var(--text);
  799. position: fixed;
  800. top: 0;
  801. left: 0;
  802. width: 100%;
  803. height: 100%;
  804. background-color: rgba(0, 0, 0, 0.75);
  805. z-index: 2147483646;
  806. display: flex;
  807. flex-direction: column;
  808. align-items: center;
  809. justify-content: center;
  810. }
  811. #pluginConfig .main {
  812. background-color: var(--body);
  813. padding: 24px;
  814. margin: 10px;
  815. overflow-y: auto;
  816. width: 400px;
  817. }
  818. #pluginConfig .buttonList {
  819. display: flex;
  820. flex-direction: row;
  821. justify-content: center;
  822. }
  823. @media (max-width: 640px) {
  824. #pluginConfig .main {
  825. width: 100%;
  826. }
  827. }
  828. #pluginConfig button {
  829. background-color: blue;
  830. margin: 0px 20px 0px 20px;
  831. padding: 10px 20px;
  832. color: white;
  833. font-size: 18px;
  834. border: none;
  835. border-radius: 4px;
  836. cursor: pointer;
  837. }
  838. #pluginConfig button {
  839. background-color: blue;
  840. }
  841. #pluginConfig button[disabled] {
  842. background-color: darkgray;
  843. cursor: not-allowed;
  844. }
  845. #pluginConfig p {
  846. display: flex;
  847. flex-direction: column;
  848. }
  849. #pluginConfig p label{
  850. display: flex;
  851. flex-direction: column;
  852. margin: 5px 0 5px 0;
  853. }
  854. #pluginConfig .inputRadioLine {
  855. display: flex;
  856. align-items: center;
  857. flex-direction: row;
  858. justify-content: space-between;
  859. }
  860. #pluginConfig input[type="text"], #pluginConfig input[type="password"] {
  861. outline: none;
  862. border-top: none;
  863. border-right: none;
  864. border-left: none;
  865. border-image: initial;
  866. border-bottom: 1px solid var(--muted);
  867. line-height: 1;
  868. height: 30px;
  869. box-sizing: border-box;
  870. width: 100%;
  871. background-color: var(--body);
  872. color: var(--text);
  873. }
  874. #pluginConfig input[type='checkbox'].switch{
  875. outline: none;
  876. appearance: none;
  877. -webkit-appearance: none;
  878. -moz-appearance: none;
  879. position: relative;
  880. width: 40px;
  881. height: 20px;
  882. background: #ccc;
  883. border-radius: 10px;
  884. transition: border-color .2s, background-color .2s;
  885. }
  886. #pluginConfig input[type='checkbox'].switch::after {
  887. content: '';
  888. display: inline-block;
  889. width: 40%;
  890. height: 80%;
  891. border-radius: 50%;
  892. background: #fff;
  893. box-shadow: 0, 0, 2px, #999;
  894. transition: .2s;
  895. top: 2px;
  896. position: absolute;
  897. right: 55%;
  898. }
  899. #pluginConfig input[type='checkbox'].switch:checked {
  900. background: rgb(19, 206, 102);
  901. }
  902. #pluginConfig input[type='checkbox'].switch:checked::after {
  903. content: '';
  904. position: absolute;
  905. right: 2px;
  906. top: 2px;
  907. }
  908.  
  909. #pluginOverlay {
  910. position: fixed;
  911. top: 0;
  912. left: 0;
  913. width: 100%;
  914. height: 100%;
  915. background-color: rgba(0, 0, 0, 0.75);
  916. z-index: 2147483645;
  917. display: flex;
  918. flex-direction: column;
  919. align-items: center;
  920. justify-content: center;
  921. }
  922. #pluginOverlay .main {
  923. color: white;
  924. font-size: 24px;
  925. width: 60%;
  926. background-color: rgba(64, 64, 64, 0.75);
  927. padding: 24px;
  928. margin: 10px;
  929. overflow-y: auto;
  930. }
  931. @media (max-width: 640px) {
  932. #pluginOverlay .main {
  933. width: 100%;
  934. }
  935. }
  936. #pluginOverlay button {
  937. padding: 10px 20px;
  938. color: white;
  939. font-size: 18px;
  940. border: none;
  941. border-radius: 4px;
  942. cursor: pointer;
  943. }
  944. #pluginOverlay button {
  945. background-color: blue;
  946. }
  947. #pluginOverlay button[disabled] {
  948. background-color: darkgray;
  949. cursor: not-allowed;
  950. }
  951. #pluginOverlay .checkbox {
  952. width: 32px;
  953. height: 32px;
  954. margin: 0 4px 0 0;
  955. padding: 0;
  956. }
  957. #pluginOverlay .checkbox-container {
  958. display: flex;
  959. align-items: center;
  960. margin: 0 0 10px 0;
  961. }
  962. #pluginOverlay .checkbox-label {
  963. color: white;
  964. font-size: 32px;
  965. font-weight: bold;
  966. margin-left: 10px;
  967. display: flex;
  968. align-items: center;
  969. }
  970.  
  971. .toastify h3 {
  972. margin: 0 0 10px 0;
  973. }
  974. .toastify p {
  975. margin: 0 ;
  976. }
  977. `);
  978. var i18n = new I18N();
  979. var config = new Config();
  980. var editConfig = new configEdit(config);
  981. var pluginMenu = new menu();
  982. function toastNode(body, title) {
  983. return renderNode({
  984. nodeType: 'div',
  985. childs: [
  986. !isNull(title) && !title.isEmpty() ? {
  987. nodeType: 'h3',
  988. childs: `%#appName#% - ${title}`
  989. } : {
  990. nodeType: 'h3',
  991. childs: '%#appName#%'
  992. },
  993. {
  994. nodeType: 'p',
  995. childs: body
  996. }
  997. ]
  998. });
  999. }
  1000. function getTextNode(node) {
  1001. return node.nodeType === Node.TEXT_NODE
  1002. ? node.textContent || ''
  1003. : node.nodeType === Node.ELEMENT_NODE
  1004. ? Array.from(node.childNodes)
  1005. .map(getTextNode)
  1006. .join('')
  1007. : '';
  1008. }
  1009. function newToast(type, params) {
  1010. const logFunc = {
  1011. [ToastType.Warn]: console.warn,
  1012. [ToastType.Error]: console.error,
  1013. [ToastType.Log]: console.log,
  1014. [ToastType.Info]: console.info,
  1015. }[type] || console.log;
  1016. params = Object.assign({
  1017. newWindow: true,
  1018. gravity: 'top',
  1019. position: 'left',
  1020. stopOnFocus: true
  1021. }, type === ToastType.Warn && {
  1022. duration: -1,
  1023. style: {
  1024. background: 'linear-gradient(-30deg, rgb(119 76 0), rgb(255 165 0))'
  1025. }
  1026. }, type === ToastType.Error && {
  1027. duration: -1,
  1028. style: {
  1029. background: 'linear-gradient(-30deg, rgb(108 0 0), rgb(215 0 0))'
  1030. }
  1031. }, !isNull(params) && params);
  1032. if (!isNull(params.text)) {
  1033. params.text = params.text.replaceVariable(i18n[language()]).toString();
  1034. }
  1035. logFunc((!isNull(params.text) ? params.text : !isNull(params.node) ? getTextNode(params.node) : 'undefined').replaceVariable(i18n[language()]));
  1036. return Toastify(params);
  1037. }
  1038. function analyzeLocalPath(path) {
  1039. let matchPath = path.replaceAll('//', '/').replaceAll('\\\\', '/').match(/^([a-zA-Z]:)?[\/\\]?([^\/\\]+[\/\\])*([^\/\\]+\.\w+)$/);
  1040. if (isNull(matchPath))
  1041. throw new Error(`%#downloadPathError#%["${path}"]`);
  1042. try {
  1043. return {
  1044. fullPath: matchPath[0],
  1045. drive: matchPath[1] || '',
  1046. filename: matchPath[3]
  1047. };
  1048. }
  1049. catch (error) {
  1050. throw new Error(`%#downloadPathError#% ["${matchPath.join(',')}"]`);
  1051. }
  1052. }
  1053. function pushDownloadTask(fileInfo) {
  1054. switch (config.downloadType) {
  1055. case DownloadType.Aria2:
  1056. aria2Download(fileInfo);
  1057. break;
  1058. case DownloadType.Browser:
  1059. browserDownload(fileInfo);
  1060. break;
  1061. default:
  1062. othersDownload(fileInfo);
  1063. break;
  1064. }
  1065. }
  1066. async function manageDownloadTaskQueue() {
  1067. let list = document.querySelectorAll('div[fileid]');
  1068. let size = list.length;
  1069. let node = renderNode({
  1070. nodeType: 'p',
  1071. childs: `%#parsingProgress#%[${list.length}/${size}]`
  1072. });
  1073. let start = newToast(ToastType.Info, {
  1074. node: node,
  1075. duration: -1
  1076. });
  1077. start.showToast();
  1078. for (let index = 0; index < list.length; index++) {
  1079. pushDownloadTask(await (new FileInfo()).init(list[index]));
  1080. node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.length - (index + 1)}/${size}]`;
  1081. }
  1082. start.hideToast();
  1083. if (size != 1) {
  1084. let completed = newToast(ToastType.Info, {
  1085. text: `%#allCompleted#%`,
  1086. duration: -1,
  1087. close: true,
  1088. onClick() {
  1089. completed.hideToast();
  1090. }
  1091. });
  1092. completed.showToast();
  1093. }
  1094. }
  1095. function aria2Download(fileInfo) {
  1096. (async function (name, downloadUrl) {
  1097. let localPath = analyzeLocalPath(config.downloadPath.replaceVariable({
  1098. FileName: name
  1099. }).trim());
  1100. let res = await aria2API('aria2.addUri', [
  1101. [downloadUrl.href],
  1102. prune({
  1103. 'all-proxy': config.downloadProxy,
  1104. 'all-proxy-user': config.downloadProxyUser,
  1105. 'all-proxy-passwd': config.downloadProxyPassword,
  1106. 'out': localPath.filename,
  1107. 'dir': localPath.fullPath.replace(localPath.filename, ''),
  1108. 'referer': window.location.hostname,
  1109. 'header': [
  1110. 'Cookie:' + unsafeWindow.document.cookie
  1111. ]
  1112. })
  1113. ]);
  1114. console.log(`Aria2 ${name} ${JSON.stringify(res)}`);
  1115. newToast(ToastType.Info, {
  1116. node: toastNode(`${name} %#pushTaskSucceed#%`)
  1117. }).showToast();
  1118. }(fileInfo.name, fileInfo.url));
  1119. }
  1120. function othersDownload(fileInfo) {
  1121. (async function (Name, DownloadUrl) {
  1122. DownloadUrl.searchParams.set('download', analyzeLocalPath(config.downloadPath.replaceVariable({
  1123. FileName: Name
  1124. }).trim()).filename);
  1125. GM_openInTab(DownloadUrl.href, { active: false, insert: true, setParent: true });
  1126. }(fileInfo.name, fileInfo.url));
  1127. }
  1128. function browserDownload(fileInfo) {
  1129. (async function (Name, DownloadUrl) {
  1130. function browserDownloadError(error) {
  1131. let errorInfo = getString(Error);
  1132. if (!(error instanceof Error)) {
  1133. errorInfo = {
  1134. 'not_enabled': `%#browserDownloadNotEnabled#%`,
  1135. 'not_whitelisted': `%#browserDownloadNotWhitelisted#%`,
  1136. 'not_permitted': `%#browserDownloadNotPermitted#%`,
  1137. 'not_supported': `%#browserDownloadNotSupported#%`,
  1138. 'not_succeeded': `%#browserDownloadNotSucceeded#% ${error.details ?? getString(error.details)}`
  1139. }[error.error] || `%#browserDownloadUnknownError#%`;
  1140. }
  1141. let toast = newToast(ToastType.Error, {
  1142. node: toastNode([
  1143. `${Name} %#downloadFailed#%`,
  1144. { nodeType: 'br' },
  1145. errorInfo,
  1146. { nodeType: 'br' },
  1147. `%#tryRestartingDownload#%`
  1148. ], '%#browserDownload#%'),
  1149. async onClick() {
  1150. toast.hideToast();
  1151. }
  1152. });
  1153. toast.showToast();
  1154. }
  1155. GM_download({
  1156. url: DownloadUrl.href,
  1157. saveAs: false,
  1158. name: config.downloadPath.replaceVariable({
  1159. NowTime: new Date(),
  1160. FileName: Name
  1161. }).trim(),
  1162. onerror: (err) => browserDownloadError(err),
  1163. ontimeout: () => browserDownloadError(new Error('%#browserDownloadTimeout#%'))
  1164. });
  1165. }(fileInfo.name, fileInfo.url));
  1166. }
  1167. async function aria2API(method, params) {
  1168. return await (await fetch(config.aria2Path, {
  1169. headers: {
  1170. 'accept': 'application/json',
  1171. 'content-type': 'application/json'
  1172. },
  1173. body: JSON.stringify({
  1174. jsonrpc: '2.0',
  1175. method: method,
  1176. id: UUID(),
  1177. params: [`token:${config.aria2Token}`, ...params]
  1178. }),
  1179. method: 'POST'
  1180. })).json();
  1181. }
  1182. async function EnvCheck() {
  1183. try {
  1184. if (GM_info.downloadMode !== 'browser') {
  1185. GM_getValue('isDebug') && console.log(GM_info);
  1186. throw new Error('%#browserDownloadModeError#%');
  1187. }
  1188. }
  1189. catch (error) {
  1190. let toast = newToast(ToastType.Error, {
  1191. node: toastNode([
  1192. `%#configError#%`,
  1193. { nodeType: 'br' },
  1194. getString(error)
  1195. ], '%#settingsCheck#%'),
  1196. position: 'center',
  1197. onClick() {
  1198. toast.hideToast();
  1199. }
  1200. });
  1201. toast.showToast();
  1202. return false;
  1203. }
  1204. return true;
  1205. }
  1206. async function aria2Check() {
  1207. try {
  1208. let res = await (await fetch(config.aria2Path, {
  1209. method: 'POST',
  1210. headers: {
  1211. 'accept': 'application/json',
  1212. 'content-type': 'application/json'
  1213. },
  1214. body: JSON.stringify({
  1215. 'jsonrpc': '2.0',
  1216. 'method': 'aria2.tellActive',
  1217. 'id': UUID(),
  1218. 'params': ['token:' + config.aria2Token]
  1219. })
  1220. })).json();
  1221. if (res.error) {
  1222. throw new Error(res.error.message);
  1223. }
  1224. }
  1225. catch (error) {
  1226. let toast = newToast(ToastType.Error, {
  1227. node: toastNode([
  1228. `Aria2 RPC %#connectionTest#%`,
  1229. { nodeType: 'br' },
  1230. getString(error)
  1231. ], '%#settingsCheck#%'),
  1232. position: 'center',
  1233. onClick() {
  1234. toast.hideToast();
  1235. }
  1236. });
  1237. toast.showToast();
  1238. return false;
  1239. }
  1240. return true;
  1241. }
  1242. function firstRun() {
  1243. console.log('First run config reset!');
  1244. GM_listValues().forEach(i => GM_deleteValue(i));
  1245. config = new Config();
  1246. editConfig = new configEdit(config);
  1247. let confirmButton = renderNode({
  1248. nodeType: 'button',
  1249. attributes: {
  1250. disabled: true,
  1251. title: i18n[language()].ok
  1252. },
  1253. childs: '%#ok#%',
  1254. events: {
  1255. click: () => {
  1256. GM_setValue('isFirstRun', false);
  1257. GM_setValue('version', GM_info.script.version);
  1258. unsafeWindow.document.querySelector('#pluginOverlay').remove();
  1259. editConfig.inject();
  1260. }
  1261. }
  1262. });
  1263. originalNodeAppendChild.call(unsafeWindow.document.body, renderNode({
  1264. nodeType: 'div',
  1265. attributes: {
  1266. id: 'pluginOverlay'
  1267. },
  1268. childs: [
  1269. {
  1270. nodeType: 'div',
  1271. className: 'main',
  1272. childs: [
  1273. { nodeType: 'p', childs: '%#useHelpForBase#%' }
  1274. ]
  1275. },
  1276. {
  1277. nodeType: 'div',
  1278. className: 'checkbox-container',
  1279. childs: {
  1280. nodeType: 'label',
  1281. className: ['checkbox-label', 'rainbow-text'],
  1282. childs: [{
  1283. nodeType: 'input',
  1284. className: 'checkbox',
  1285. attributes: {
  1286. type: 'checkbox',
  1287. name: 'agree-checkbox'
  1288. },
  1289. events: {
  1290. change: (event) => {
  1291. confirmButton.disabled = !event.target.checked;
  1292. }
  1293. }
  1294. }, '%#alreadyKnowHowToUse#%']
  1295. }
  1296. },
  1297. confirmButton
  1298. ]
  1299. }));
  1300. }
  1301. async function main() {
  1302. if (GM_getValue('isFirstRun', true)) {
  1303. firstRun();
  1304. return;
  1305. }
  1306. if (!await config.check()) {
  1307. newToast(ToastType.Info, {
  1308. text: `%#configError#%`,
  1309. duration: 60 * 1000,
  1310. }).showToast();
  1311. editConfig.inject();
  1312. return;
  1313. }
  1314. GM_setValue('version', GM_info.script.version);
  1315. pluginMenu.inject();
  1316. let notice = newToast(ToastType.Info, {
  1317. node: toastNode([
  1318. `加载完成`,
  1319. { nodeType: 'br' },
  1320. `公告: `,
  1321. ...i18n[language()].notice
  1322. ]),
  1323. duration: 10000,
  1324. gravity: 'bottom',
  1325. position: 'center',
  1326. onClick() {
  1327. notice.hideToast();
  1328. }
  1329. });
  1330. notice.showToast();
  1331. }
  1332. if (new Version(GM_getValue('version', '0.0.0')).compare(new Version('0.0.1')) === VersionState.Low) {
  1333. GM_setValue('isFirstRun', true);
  1334. alert(i18n[language()].configurationIncompatible);
  1335. }
  1336. (unsafeWindow.document.body ? Promise.resolve() : new Promise(resolve => originalAddEventListener.call(unsafeWindow.document, "DOMContentLoaded", resolve))).then(main);
  1337. })();