Unlock Hath Perks

Unlock Hath Perks and add other helpers

  1. // ==UserScript==
  2. // @name Unlock Hath Perks
  3. // @name:zh-TW 解鎖 Hath Perks
  4. // @name:zh-CN 解锁 Hath Perks
  5. // @description Unlock Hath Perks and add other helpers
  6. // @description:zh-TW 解鎖 Hath Perks 及增加一些小工具
  7. // @description:zh-CN 解锁 Hath Perks 及增加一些小工具
  8. // @namespace https://flandre.in/github
  9. // @version 2.2.3
  10. // @match https://e-hentai.org/*
  11. // @match https://exhentai.org/*
  12. // @require https://unpkg.com/vue@2.6.9/dist/vue.min.js
  13. // @icon https://i.imgur.com/JsU0vTd.png
  14. // @grant GM_getValue
  15. // @grant GM.getValue
  16. // @grant GM_setValue
  17. // @grant GM.setValue
  18. // @noframes
  19. // @author FlandreDaisuki
  20. // @supportURL https://github.com/FlandreDaisuki/My-Browser-Extensions/issues
  21. // @homepageURL https://github.com/FlandreDaisuki/My-Browser-Extensions/blob/master/userscripts/UnlockHathPerks/README.md
  22. // @license MPLv2
  23. // ==/UserScript==
  24.  
  25. (function () {
  26. 'use strict';
  27.  
  28. const noop = () => {};
  29.  
  30. const $find = (el, selectors) => el.querySelector(selectors);
  31. const $ = (selectors) => document.querySelector(selectors);
  32.  
  33. const $el = (tag, attr = {}, cb = noop) => {
  34. const el = document.createElement(tag);
  35. if (typeof(attr) === 'string') {
  36. el.textContent = attr;
  37. }
  38. else {
  39. Object.assign(el, attr);
  40. }
  41. cb(el);
  42. return el;
  43. };
  44.  
  45. const $style = (stylesheet) => $el('style', stylesheet, (el) => document.head.appendChild(el));
  46.  
  47. const throttle = (fn, timeout = 1000) => {
  48. let locked = false;
  49. return (...args) => {
  50. if (!locked){
  51. locked = true;
  52. fn(...args);
  53. setTimeout(() => {
  54. locked = false;
  55. }, timeout);
  56. }
  57. };
  58. };
  59.  
  60. /* cSpell:ignore navdiv navbtn exhentai adsbyjuicy searchbox favcat searchnav favform */
  61. /* cSpell:ignoreRegExp \b\.\w+\b */
  62. /* eslint-disable no-console */
  63. /* global Vue */
  64.  
  65. // #region easy DOM
  66.  
  67. // nav
  68. const nb = $('#nb');
  69. const navdiv = $el('div');
  70. const navbtn = $el('a', {
  71. id: 'uhp-btn',
  72. textContent: 'Unlock Hath Perks',
  73. });
  74. navbtn.addEventListener('click', () => {
  75. $('#uhp-panel-container').classList.remove('hidden');
  76. });
  77. nb.appendChild(navdiv);
  78. navdiv.appendChild(navbtn);
  79.  
  80. // panel container
  81. const uhpPanelContainer = $el('div', {
  82. className: 'hidden',
  83. id: 'uhp-panel-container',
  84. });
  85. uhpPanelContainer.addEventListener('click', () => {
  86. uhpPanelContainer.classList.add('hidden');
  87. });
  88. document.body.appendChild(uhpPanelContainer);
  89.  
  90. // panel
  91. const uhpPanel = $el('div', { id: 'uhp-panel' }, (el) => {
  92. if (location.host === 'exhentai.org') {
  93. el.classList.add('dark');
  94. }
  95. el.addEventListener('click', (ev) => ev.stopPropagation());
  96. });
  97. uhpPanelContainer.appendChild(uhpPanel);
  98.  
  99. // #endregion easy DOM
  100.  
  101. // #region configs and events
  102.  
  103. const uhpConfig = {
  104. abg: true,
  105. mt: true,
  106. pe: true,
  107. };
  108.  
  109. Object.assign(uhpConfig, GM_getValue('uhp', uhpConfig));
  110. GM_setValue('uhp', uhpConfig);
  111.  
  112. if (uhpConfig.abg) {
  113. Object.defineProperty(window, 'adsbyjuicy', {
  114. configurable: false,
  115. enumerable: false,
  116. writable: false,
  117. value: Object.create(null),
  118. });
  119. }
  120.  
  121.  
  122. // More Thumbs code block
  123. if (location.pathname.startsWith('/g/')) {
  124. (async() => {
  125. const getGalleryPageState = async(url, selectors) => {
  126. const result = {
  127. elements: [],
  128. nextURL: null,
  129. };
  130.  
  131. if (!url) { return result; }
  132.  
  133. const resp = await fetch(url, {
  134. credentials: 'same-origin',
  135. });
  136.  
  137. if (resp.ok) {
  138. const html = await resp.text();
  139. const docEl = (new DOMParser())
  140. .parseFromString(html, 'text/html')
  141. .documentElement;
  142. result.elements = [...$find(docEl, selectors.parent)?.children ?? []];
  143.  
  144. const nextEl = $find(docEl, selectors.np);
  145. result.nextURL = nextEl ? (nextEl.href || null) : null;
  146. }
  147.  
  148. console.log(result);
  149. return result;
  150. };
  151.  
  152. const selectors = {
  153. np: '.ptt td:last-child > a',
  154. parent: '#gdt',
  155. };
  156.  
  157. const pageState = {
  158. parent: $(selectors.parent),
  159. locked: false,
  160. nextURL: null,
  161. };
  162.  
  163. const thisPage = await getGalleryPageState(location.href, selectors);
  164.  
  165. while (pageState.parent.firstChild) {
  166. pageState.parent.firstChild.remove();
  167. }
  168.  
  169. thisPage.elements
  170. .filter((el) => !el.classList.contains('c'))
  171. .forEach((el) => pageState.parent.appendChild(el));
  172. pageState.nextURL = thisPage.nextURL;
  173. if (!pageState.nextURL) {
  174. return;
  175. }
  176.  
  177. if (uhpConfig.mt) {
  178. // search page found results
  179.  
  180. document.addEventListener('scroll', throttle(async() => {
  181. const anchorTop = $('table.ptb').getBoundingClientRect().top;
  182. const vh = window.innerHeight;
  183.  
  184. if (anchorTop < vh * 2 && !pageState.lock && pageState.nextURL) {
  185. pageState.lock = true;
  186.  
  187. const nextPage = await getGalleryPageState(pageState.nextURL, selectors);
  188. nextPage.elements
  189. .filter((el) => !el.classList.contains('c'))
  190. .forEach((el) => pageState.parent.appendChild(el));
  191. pageState.nextURL = nextPage.nextURL;
  192.  
  193. pageState.lock = false;
  194. }
  195. }));
  196. }
  197. })();
  198. }
  199.  
  200. // Page Enlargement code block
  201. if ($('input[name="f_search"]') && $('.itg')) {
  202. (async() => {
  203. const getPageState = async(url, selectors) => {
  204. const result = {
  205. elements: [],
  206. nextURL: null,
  207. };
  208.  
  209. if (!url) { return result; }
  210.  
  211. const resp = await fetch(url, {
  212. credentials: 'same-origin',
  213. });
  214.  
  215. if (resp.ok) {
  216. const html = await resp.text();
  217. const docEl = (new DOMParser())
  218. .parseFromString(html, 'text/html')
  219. .documentElement;
  220. result.elements = [...$find(docEl, selectors.parent)?.children ?? []];
  221.  
  222. const nextEl = $find(docEl, selectors.np);
  223. result.nextURL = nextEl ? (nextEl.href || null) : null;
  224. }
  225.  
  226. console.log(result);
  227. return result;
  228. };
  229.  
  230. const isTableLayout = Boolean($('table.itg'));
  231. const status = $el('h1', { textContent: 'Loading...', id: 'uhp-status' });
  232. const selectors = {
  233. np: '.ptt td:last-child > a, .searchnav a[href*="next="]',
  234. parent: isTableLayout ? 'table.itg > tbody' : 'div.itg',
  235. };
  236.  
  237. const pageState = {
  238. parent: $(selectors.parent),
  239. locked: false,
  240. nextURL: null,
  241. };
  242.  
  243. const thisPage = await getPageState(location.href, selectors);
  244.  
  245. while (pageState.parent.firstChild) {
  246. pageState.parent.firstChild.remove();
  247. }
  248.  
  249. thisPage.elements.forEach((el) => pageState.parent.appendChild(el));
  250. pageState.nextURL = thisPage.nextURL;
  251. if (!pageState.nextURL) {
  252. status.textContent = 'End';
  253. }
  254.  
  255. if (uhpConfig.pe) {
  256. $('table.ptb, .itg + .searchnav, #favform + .searchnav').replaceWith(status);
  257.  
  258. // search page found results
  259.  
  260. document.addEventListener('scroll', async() => {
  261. const anchorTop = status.getBoundingClientRect().top;
  262. const vh = window.innerHeight;
  263.  
  264. if (anchorTop < vh * 2 && !pageState.lock && pageState.nextURL) {
  265. pageState.lock = true;
  266.  
  267. const nextPage = await getPageState(pageState.nextURL, selectors);
  268. nextPage.elements.forEach((el) => pageState.parent.appendChild(el));
  269. pageState.nextURL = nextPage.nextURL;
  270. if (!pageState.nextURL) {
  271. status.textContent = 'End';
  272. }
  273. pageState.lock = false;
  274. }
  275. });
  276. }
  277. })();
  278. }
  279.  
  280. // #endregion configs and events
  281.  
  282.  
  283. const uhpPanelTemplate = `
  284. <div id="uhp-panel" :class="{ dark: isExH }" @click.stop>
  285. <h1>Hath Perks</h1>
  286. <div>
  287. <div v-for="d in HathPerks" class="option-grid">
  288. <div class="material-switch">
  289. <input :id="getConfId(d.abbr)" type="checkbox" v-model="conf[d.abbr]" @change="save" />
  290. <label :for="getConfId(d.abbr)"></label>
  291. </div>
  292. <span class="uhp-conf-title">{{d.title}}</span>
  293. <span class="uhp-conf-desc">{{d.desc}}</span>
  294. </div>
  295. </div>
  296. </div>
  297. `;
  298.  
  299. // eslint-disable-next-line no-new
  300. new Vue({
  301. el: '#uhp-panel',
  302. template: uhpPanelTemplate,
  303. data: {
  304. conf: uhpConfig,
  305. HathPerks: [{
  306. abbr: 'abg',
  307. title: 'Ads-Be-Gone',
  308. desc: 'Remove ads. You can use it with adblock webextensions.',
  309. }, {
  310. abbr: 'mt',
  311. title: 'More Thumbs',
  312. desc: 'Scroll infinitely in gallery pages.',
  313. }, {
  314. abbr: 'pe',
  315. title: 'Paging Enlargement',
  316. desc: 'Scroll infinitely in search results pages.',
  317. }],
  318. Others: [],
  319. },
  320. computed: {
  321. isExH() { return location.host === 'exhentai.org'; },
  322. },
  323. methods: {
  324. save() { GM_setValue('uhp', uhpConfig); },
  325. getConfId(id) { return `ubp-conf-${ id }`; },
  326. },
  327. });
  328.  
  329. $style(`
  330. /* nav bar */
  331. #nb {
  332. width: initial;
  333. max-width: initial;
  334. max-height: initial;
  335. justify-content: center;
  336. }
  337.  
  338. /* search input */
  339. table.itc + p.nopm {
  340. display: flex;
  341. flex-flow: row wrap;
  342. justify-content: center;
  343. }
  344. input[name="f_search"] {
  345. width: 100%;
  346. }
  347.  
  348. /* /favorites.php */
  349. input[name="favcat"] + div {
  350. display: flex;
  351. flex-flow: row wrap;
  352. justify-content: center;
  353. gap: 8px;
  354. }
  355.  
  356. /* gallery grid */
  357. .gl1t {
  358. display: flex;
  359. flex-flow: column;
  360. }
  361. .gl1t > .gl3t {
  362. flex: 1;
  363. }
  364. .gl1t > .gl3t > a {
  365. display: flex;
  366. align-items: center;
  367. justify-content: center;
  368. height: 100%;
  369. }`);
  370.  
  371. $style(`
  372. /* uhp */
  373. #uhp-btn {
  374. cursor: pointer;
  375. }
  376. #uhp-panel-container {
  377. position: fixed;
  378. top: 0;
  379. height: 100vh;
  380. width: 100vw;
  381. background-color: rgba(200, 200, 200, 0.7);
  382. z-index: 2;
  383. display: flex;
  384. align-items: center;
  385. justify-content: center;
  386. }
  387. #uhp-panel-container.hidden {
  388. visibility: hidden;
  389. opacity: 0;
  390. }
  391. #uhp-panel {
  392. padding: 1.2rem;
  393. background-color: floralwhite;
  394. border-radius: 1rem;
  395. font-size: 1rem;
  396. color: darkred;
  397. max-width: 650px;
  398. }
  399. #uhp-panel.dark {
  400. background-color: dimgray;
  401. color: ghostwhite;
  402. }
  403. #uhp-panel .option-grid {
  404. display: grid;
  405. grid-template-columns: max-content 120px 1fr;
  406. grid-gap: 0.5rem 1rem;
  407. margin: 0.5rem 1rem;
  408. }
  409. #uhp-panel .option-grid > * {
  410. display: flex;
  411. justify-content: center;
  412. align-items: center;
  413. }
  414. #uhp-full-width-container.fullwidth,
  415. #uhp-full-width-container.fullwidth div.itg {
  416. max-width: none;
  417. }
  418. #uhp-full-width-container div.itg {
  419. display: grid;
  420. grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  421. grid-gap: 2px;
  422. }
  423. #uhp-full-width-container div.itg.uhp-tpf-dense {
  424. grid-auto-flow: dense;
  425. }
  426. #uhp-full-width-container div.id1 {
  427. height: 345px;
  428. float: none;
  429. display: flex;
  430. flex-direction: column;
  431. margin: 3px auto;
  432. padding: 4px 0;
  433. }
  434. #uhp-full-width-container div.id2 {
  435. overflow: visible;
  436. height: initial;
  437. margin: 4px auto;
  438. }
  439. #uhp-full-width-container div.id3 {
  440. flex: 1;
  441. display: flex;
  442. justify-content: center;
  443. align-items: center;
  444. }
  445. .uhp-list-parent-eh tr:nth-of-type(2n+1) {
  446. background-color: #EDEBDF;
  447. }
  448. .uhp-list-parent-eh tr:nth-of-type(2n+2) {
  449. background-color: #F2F0E4;
  450. }
  451. .uhp-list-parent-exh tr:nth-of-type(2n+1) {
  452. background-color: #363940;
  453. }
  454. .uhp-list-parent-exh tr:nth-of-type(2n+2) {
  455. background-color: #4F535B;
  456. }
  457. #uhp-status {
  458. text-align: center;
  459. font-size: 3rem;
  460. clear: both;
  461. padding: 2rem 0;
  462. }
  463.  
  464. /* https://bootsnipp.com/snippets/featured/material-design-switch */
  465. .material-switch {
  466. display: inline-block;
  467. }
  468.  
  469. .material-switch > input[type="checkbox"] {
  470. display: none;
  471. }
  472.  
  473. .material-switch > input[type="checkbox"] + label {
  474. display: inline-block;
  475. position: relative;
  476. margin: 6px;
  477. border-radius: 8px;
  478. width: 40px;
  479. height: 16px;
  480. opacity: 0.3;
  481. background-color: rgb(0, 0, 0);
  482. box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5);
  483. transition: all 0.4s ease-in-out;
  484. }
  485.  
  486. .material-switch > input[type="checkbox"] + label::after {
  487. position: absolute;
  488. top: -4px;
  489. left: -4px;
  490. border-radius: 16px;
  491. width: 24px;
  492. height: 24px;
  493. content: "";
  494. background-color: rgb(255, 255, 255);
  495. box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
  496. transition: all 0.3s ease-in-out;
  497. }
  498.  
  499. .material-switch > input[type="checkbox"]:checked + label {
  500. background-color: #0e0;
  501. opacity: 0.7;
  502. }
  503.  
  504. .material-switch > input[type="checkbox"]:checked + label::after {
  505. background-color: inherit;
  506. left: 20px;
  507. }
  508. .material-switch > input[type="checkbox"]:disabled + label::after {
  509. content: "\\f023";
  510. line-height: 24px;
  511. font-size: 0.8em;
  512. font-family: FontAwesome;
  513. color: initial;
  514. }`);
  515.  
  516. $el('link', {
  517. href: 'https://use.fontawesome.com/releases/v5.8.0/css/all.css',
  518. rel: 'stylesheet',
  519. integrity: 'sha384-Mmxa0mLqhmOeaE8vgOSbKacftZcsNYDjQzuCOm6D02luYSzBG8vpaOykv9lFQ51Y',
  520. crossOrigin: 'anonymous',
  521. }, (el) => document.head.appendChild(el));
  522.  
  523. })();