Sleazy Fork is available in English.

HV Utils URL CN

zh-cn

  1. // ==UserScript==
  2. // @name HV Utils URL CN
  3. // @namespace HVUT
  4. // @homepageURL https://forums.e-hentai.org/index.php?showtopic=211883
  5. // @supportURL https://forums.e-hentai.org/index.php?showtopic=211883
  6. // @version 1.3.0
  7. // @date 2023-12-31
  8. // @author sssss2
  9. // @description zh-cn
  10. // @match *://*.hentaiverse.org/?s=Battle
  11. // @match *://*.hentaiverse.org/isekai/?s=Battle
  12. // @connect hentaiverse.org
  13. // @connect e-hentai.org
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_xmlhttpRequest
  17. // @run-at document-end
  18. // ==/UserScript==
  19.  
  20. var settings = {
  21.  
  22. randomEncounter: true, // 随机遭遇通知
  23. reBeep: { volume: 0.2, frequency: 500, time: 0.5, delay: 1 }, // 当随机遭遇准备就绪时发出蜂鸣声;将音量设置为 0 以禁用
  24. reBattleCSS: 'top: 10px; left: 600px; position: absolute; cursor: pointer; font-size: 10pt; font-weight: bold;', // 修改顶部和左侧以定位计时器
  25. ajaxRound: true, // 支持 Monsterbation
  26.  
  27. };
  28.  
  29. /* eslint-disable arrow-spacing, block-spacing, comma-spacing, key-spacing, keyword-spacing, object-curly-spacing, space-before-blocks, space-before-function-paren, space-infix-ops, semi-spacing */
  30. function $id(id, d) {return (d || document).getElementById(id);}
  31. function $doc(h) {const d = document.implementation.createHTMLDocument(''); d.documentElement.innerHTML = h; return d;}
  32. function $element(t, p, a, f) {let e; if (t) {e = document.createElement(t);} else if (t === '') {e = document.createTextNode(a); a = null;} else {return document.createDocumentFragment();} if (a !== null && a !== undefined) {function ao(e, a) {Object.entries(a).forEach(([an, av]) => {if (typeof av === 'object') {let a; if (an in e) {a = e[an];} else {e[an] = {}; a = e[an];} Object.entries(av).forEach(([an, av]) => {a[an] = av;});} else {if (an === 'style') {e.style.cssText = av;} else if (an in e) {e[an] = av;} else {e.setAttribute(an, av);}}});} function as(e, a) {const an = {'#': 'id', '.': 'className', '!': 'style', '/': 'innerHTML'}[a[0]]; if (an) {e[an] = a.slice(1);} else if (a !== '') {e.textContent = a;}} if (typeof a === 'string' || typeof a === 'number') {e.textContent = a;} else if (Array.isArray(a)) {a.forEach((a) => {if (typeof a === 'string' || typeof a === 'number') {as(e, a);} else if (typeof a === 'object') {ao(e, a);}});} else if (typeof a === 'object') {ao(e, a);}} if (f) {if (typeof f === 'function') {e.addEventListener('click', f);} else if (typeof f === 'object') {Object.entries(f).forEach(([ft, fl]) => {e.addEventListener(ft, fl);});}} if (p) {if (p.nodeType === 1 || p.nodeType === 11) {p.appendChild(e);} else if (Array.isArray(p)) {if (['beforebegin', 'afterbegin', 'beforeend', 'afterend'].includes(p[1])) {p[0].insertAdjacentElement(p[1], e);} else if (!isNaN(p[1])) {p[0].insertBefore(e, p[0].childNodes[p[1]]);} else {p[0].insertBefore(e, p[1]);}}} return e;}
  33. function time_format(t, o) {t = Math.floor(t / 1000); const h = Math.floor(t / 3600).toString().padStart(2, '0'); const m = Math.floor(t % 3600 / 60).toString().padStart(2, '0'); const s = (t % 60).toString().padStart(2, '0'); return !o ? `${h}:${m}:${s}` : o === 1 ? `${h}:${m}` : o === 2 ? `${m}:${s}` : '';}
  34. function play_beep(s = { volume: 0.2, frequency: 500, time: 0.5, delay: 1 }) {if (!s.volume) {return;} const c = new window.AudioContext(); const o = c.createOscillator(); const g = c.createGain(); o.type = 'sine'; o.frequency.value = s.frequency; g.gain.value = s.volume; o.connect(g); g.connect(c.destination); o.start(s.delay); o.stop(s.delay + s.time);}
  35. function popup(t, s) {function r(e) {e.preventDefault(); e.stopImmediatePropagation(); if (e.which === 1 || e.which === 13 || e.which === 27 || e.which === 32) {w.remove(); document.removeEventListener('keydown', r);}} const w = $element('div', document.body, ['!position:fixed;top:0;left:0;width:1236px;height:702px;padding:3px 100% 100% 3px;background-color:#0006;z-index:1001;cursor:pointer;display:flex;justify-content:center;align-items:center;'], r); const d = $element('div', w, ['/' + t, '!min-width:400px;min-height:100px;max-width:100%;max-height:100%;padding:10px;background-color:#fff;border:1px solid;display:flex;flex-direction:column;justify-content:center;font-size:10pt;color:#333;' + (s || '')]); document.addEventListener('keydown', r); return d;}
  36.  
  37. function getValue(k, d, p = _ns + '_') {const v = localStorage.getItem(p + k); return v === null ? d : JSON.parse(v);}
  38. function setValue(k, v, p = _ns + '_', r) {localStorage.setItem(p + k, JSON.stringify(v, r));}
  39. /* eslint-enable */
  40.  
  41. var _ns = 'hvut';
  42. var _isekai = location.pathname.includes('/isekai/');
  43. if (_isekai) {
  44. _ns = 'hvuti';
  45. _isekai = /(\d+ Season \d+)/.exec($id('world_text')?.textContent) ? RegExp.$1 : '1';
  46. }
  47.  
  48. // AJAX
  49. var $ajax = {
  50.  
  51. interval: 300, // 不要减少此数字,否则可能触发服务器的限制器,导致您被禁止
  52. max: 4,
  53. tid: null,
  54. conn: 0,
  55. index: 0,
  56. queue: [],
  57.  
  58. fetch: function (url, data, method, context = {}, headers = {}) {
  59. return new Promise((resolve, reject) => {
  60. $ajax.add(method, url, data, resolve, reject, context, headers);
  61. });
  62. },
  63. repeat: function (count, func, ...args) {
  64. const list = [];
  65. for (let i = 0; i < count; i++) {
  66. list.push(func(...args));
  67. }
  68. return list;
  69. },
  70. add: function (method, url, data, onload, onerror, context = {}, headers = {}) {
  71. if (!data) {
  72. method = 'GET';
  73. } else if (!method) {
  74. method = 'POST';
  75. }
  76. if (method === 'POST') {
  77. if (!headers['Content-Type']) {
  78. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  79. }
  80. if (data && typeof data === 'object') {
  81. data = Object.entries(data).map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&');
  82. }
  83. } else if (method === 'JSON') {
  84. method = 'POST';
  85. if (!headers['Content-Type']) {
  86. headers['Content-Type'] = 'application/json';
  87. }
  88. if (data && typeof data === 'object') {
  89. data = JSON.stringify(data);
  90. }
  91. }
  92. context.onload = onload;
  93. context.onerror = onerror;
  94. $ajax.queue.push({ method, url, data, headers, context, onload: $ajax.onload, onerror: $ajax.onerror });
  95. $ajax.next();
  96. },
  97. next: function () {
  98. if (!$ajax.queue[$ajax.index] || $ajax.error) {
  99. return;
  100. }
  101. if ($ajax.tid) {
  102. if (!$ajax.conn) {
  103. clearTimeout($ajax.tid);
  104. $ajax.timer();
  105. $ajax.send();
  106. }
  107. } else {
  108. if ($ajax.conn < $ajax.max) {
  109. $ajax.timer();
  110. $ajax.send();
  111. }
  112. }
  113. },
  114. timer: function () {
  115. $ajax.tid = setTimeout(() => {
  116. $ajax.tid = null;
  117. $ajax.next();
  118. }, $ajax.interval);
  119. },
  120. send: function () {
  121. GM_xmlhttpRequest($ajax.queue[$ajax.index]);
  122. $ajax.index++;
  123. $ajax.conn++;
  124. },
  125. onload: function (r) {
  126. $ajax.conn--;
  127. const text = r.responseText;
  128. if (r.status !== 200) {
  129. $ajax.error = `${r.status} ${r.statusText}: ${r.finalUrl}`;
  130. r.context.onerror?.();
  131. } else if (text === 'state lock limiter in effect') {
  132. if ($ajax.error !== text) {
  133. popup(`<p style="color: #f00; font-weight: bold;">${text}</p><p>Your connection speed is so fast that <br>you have reached the maximum connection limit.</p><p>Try again later.</p>`);
  134. }
  135. $ajax.error = text;
  136. r.context.onerror?.();
  137. } else {
  138. r.context.onload?.(text);
  139. $ajax.next();
  140. }
  141. },
  142. onerror: function (r) {
  143. $ajax.conn--;
  144. $ajax.error = `${r.status} ${r.statusText}: ${r.finalUrl}`;
  145. r.context.onerror?.();
  146. $ajax.next();
  147. },
  148.  
  149. };
  150.  
  151. // RANDOM ENCOUNTER
  152. var $re = {
  153.  
  154. init: function () {
  155. if ($re.inited) {
  156. return;
  157. }
  158. $re.inited = true;
  159. $re.type = (!location.hostname.includes('hentaiverse.org') || _isekai) ? 0 : $id('navbar') ? 1 : $id('battle_top') ? 2 : false;
  160. $re.get();
  161. },
  162. get: function () {
  163. $re.json = getValue('re', { date: 0, key: '', count: 0, clear: true }, 'hvut_');
  164. const gm_json = JSON.parse(GM_getValue('re', null)) || { date: -1 };
  165. if ($re.json.date === gm_json.date) {
  166. if ($re.json.clear !== gm_json.clear) {
  167. $re.json.clear = true;
  168. $re.set();
  169. }
  170. } else {
  171. if ($re.json.date < gm_json.date) {
  172. $re.json = gm_json;
  173. }
  174. $re.set();
  175. }
  176. },
  177. set: function () {
  178. setValue('re', $re.json, 'hvut_');
  179. GM_setValue('re', JSON.stringify($re.json));
  180. },
  181. reset: function () {
  182. $re.json.date = Date.now();
  183. $re.json.count = 0;
  184. $re.json.clear = true;
  185. $re.set();
  186. $re.start();
  187. },
  188. check: function () {
  189. $re.init();
  190. if (/\?s=Battle&ss=ba&encounter=([A-Za-z0-9=]+)(?:&date=(\d+))?/.test(location.search)) {
  191. const key = RegExp.$1;
  192. const date = parseInt(RegExp.$2);
  193. const now = Date.now();
  194. if ($re.json.key === key) {
  195. if (!$re.json.clear) {
  196. $re.json.clear = true;
  197. $re.set();
  198. }
  199. } else if (date) {
  200. if ($re.json.date < date) {
  201. $re.json.date = date;
  202. $re.json.key = key;
  203. $re.json.count++;
  204. $re.json.clear = true;
  205. $re.set();
  206. }
  207. } else if ($re.json.date + 1800000 < now) {
  208. $re.json.date = now;
  209. $re.json.key = key;
  210. $re.json.count++;
  211. $re.json.clear = true;
  212. $re.set();
  213. }
  214. }
  215. },
  216. clock: function (button) {
  217. $re.init();
  218. $re.button = button;
  219. $re.button.addEventListener('click', (e) => { $re.run(e.ctrlKey || e.shiftKey); });
  220. const date = new Date($re.json.date);
  221. const now = new Date();
  222. if (date.getUTCDate() !== now.getUTCDate() || date.getUTCMonth() !== now.getUTCMonth() || date.getUTCFullYear() !== now.getUTCFullYear()) {
  223. $re.reset();
  224. $re.load();
  225. }
  226. $re.check();
  227. $re.start();
  228. },
  229. refresh: function () {
  230. const remain = $re.json.date + 1800000 - Date.now();
  231. if (remain > 0) {
  232. $re.button.textContent = time_format(remain, 2) + ` [${$re.json.count}]`;
  233. $re.beep = true;
  234. } else {
  235. $re.button.textContent = (!$re.json.clear ? 'Expired' : 'Ready') + ` [${$re.json.count}]`;
  236. if ($re.beep) {
  237. $re.beep = false;
  238. play_beep(settings.reBeep);
  239. }
  240. $re.stop();
  241. }
  242. },
  243. run: async function (engage) {
  244. if ($re.type === 2) {
  245. $re.load();
  246. } else if ($re.type === 1) {
  247. if (!$re.json.clear || engage) {
  248. $re.engage();
  249. } else {
  250. $re.load(true);
  251. }
  252. } else if ($re.type === 0) {
  253. $re.stop();
  254. $re.button.textContent = 'Checking...';
  255. const html = await $ajax.fetch('https://hentaiverse.org/');
  256. if (html.includes('<div id="navbar">')) {
  257. if (!$re.json.clear || engage) {
  258. $re.engage();
  259. } else {
  260. $re.load(true);
  261. }
  262. } else {
  263. $re.load();
  264. }
  265. }
  266. },
  267. load: async function (engage) {
  268. $re.stop();
  269. $re.get();
  270. $re.button.textContent = 'Loading...';
  271. const html = await $ajax.fetch('https://e-hentai.org/news.php');
  272. const doc = $doc(html);
  273. const eventpane = $id('eventpane', doc);
  274. if (eventpane && /\?s=Battle&amp;ss=ba&amp;encounter=([A-Za-z0-9=]+)/.test(eventpane.innerHTML)) {
  275. $re.json.date = Date.now();
  276. $re.json.key = RegExp.$1;
  277. $re.json.count++;
  278. $re.json.clear = false;
  279. $re.set();
  280. if (engage) {
  281. $re.engage();
  282. return;
  283. }
  284. } else if (eventpane && /It is the dawn of a new day/.test(eventpane.innerHTML)) {
  285. popup(eventpane.innerHTML);
  286. $re.reset();
  287. } else {
  288. popup('Failed to get a new Random Encounter key');
  289. }
  290. $re.start();
  291. },
  292. engage: function () {
  293. if (!$re.json.key) {
  294. return;
  295. }
  296. const href = `?s=Battle&ss=ba&encounter=${$re.json.key}&date=${$re.json.date}`;
  297. if ($re.type === 2) {
  298. return;
  299. } else if ($re.type === 1) {
  300. location.href = href;
  301. } else if ($re.type === 0) {
  302. window.open((settings.reGalleryAlt ? 'http://alt.hentaiverse.org/' : 'https://hentaiverse.org/') + href, '_blank');
  303. $re.json.clear = true;
  304. $re.start();
  305. }
  306. },
  307. start: function () {
  308. $re.stop();
  309. if (!$re.json.clear) {
  310. $re.button.style.color = '#e00';
  311. } else {
  312. $re.button.style.color = '';
  313. }
  314. $re.tid = setInterval($re.refresh, 1000);
  315. $re.refresh();
  316. },
  317. stop: function () {
  318. if ($re.tid) {
  319. clearInterval($re.tid);
  320. $re.tid = 0;
  321. }
  322. },
  323.  
  324. };
  325.  
  326. // BATTLE
  327. if ($id('battle_top')) {
  328. if (settings.randomEncounter) {
  329. if ($id('textlog').tBodies[0].lastElementChild.textContent === 'Initializing random encounter ...') {
  330. $re.check();
  331. }
  332. const button = $element('div', $id('csp'), ['RE', '!' + (settings.reBattleCSS || 'position: absolute; top: 0px; left: 0px; cursor: pointer;')]);
  333. $re.clock(button);
  334. if (settings.ajaxRound) {
  335. (new MutationObserver(() => { if (!button.parentNode.parentNode && $id('csp')) { $id('csp').appendChild(button); }$re.start(); })).observe(document.body, { childList: true });
  336. }
  337. }
  338. } else if ($id('navbar')) {
  339. const url = getValue('url', '.');
  340. location.href = url.endsWith('/?s=Battle') ? '.' : url;
  341. }