Unlock Hath Perks

Unlock Hath Perks and add other helpers

// ==UserScript==
// @name       Unlock Hath Perks
// @name:zh-TW 解鎖 Hath Perks
// @name:zh-CN 解锁 Hath Perks
// @description       Unlock Hath Perks and add other helpers
// @description:zh-TW 解鎖 Hath Perks 及增加一些小工具
// @description:zh-CN 解锁 Hath Perks 及增加一些小工具
// @namespace   https://flandre.in/github
// @version     3.0.2
// @match       https://e-hentai.org/*
// @match       https://exhentai.org/*
// @icon        https://i.imgur.com/JsU0vTd.png
// @grant       GM_getValue
// @grant       GM_setValue
// @noframes
// @author      FlandreDaisuki
// @supportURL  https://github.com/FlandreDaisuki/My-Browser-Extensions/issues
// @homepageURL https://github.com/FlandreDaisuki/My-Browser-Extensions/blob/master/userscripts/UnlockHathPerks/README.md
// @license     MPLv2
// ==/UserScript==

(function () {
  'use strict';

  const noop = () => {};

  const $find = (el, selectors) => el.querySelector(selectors);
  const $ = (selectors) => document.querySelector(selectors);
  const $$ = (selectors) => Array.from(document.querySelectorAll(selectors));

  const $el = (tag, attr = {}, cb = noop) => {
    const el = document.createElement(tag);
    if (typeof(attr) === 'string') {
      el.textContent = attr;
    }
    else {
      Object.assign(el, attr);
    }
    cb(el);
    return el;
  };

  const $html = (htmlText) => {
    const tmpEl = $el('div');
    tmpEl.innerHTML = htmlText;
    return tmpEl.firstElementChild;
  };

  const $style = (stylesheet) => $el('style', stylesheet, (el) => document.head.appendChild(el));

  const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });

  /* cSpell:ignore exhentai juicyads favcat searchnav favform */
  /* eslint-disable no-console */

  /** @type {{abg: boolean, mt: boolean, pe: boolean, fw: boolean}} */
  const uhpConfig = (() => {
    const _conf = Object.assign({ abg: true, mt: true, pe: true, fw: false }, GM_getValue('uhp') );
    GM_setValue('uhp', _conf);

    return new Proxy(_conf, {
      set(target, propertyKey, value){
        const r = Reflect.set(target, propertyKey, value);
        GM_setValue('uhp', _conf);
        return r;
      },
    });
  })();


  // #region Ads-Be-Gone
  if (uhpConfig.abg) {
    $style('iframe[src*="juicyads"] { display:none !important; }');
  }
  // #endregion Ads-Be-Gone

  // #region More Thumbs
  if (uhpConfig.mt) {
    (async() => {
      if (!location.pathname.startsWith('/g/')){ return; }

      const NEXT_PAGE_SELECTOR = '.ptt td:last-child > a';
      const IMAGE_PARENT_SELECTOR = '#gdt';

      const imgParentEl = $(IMAGE_PARENT_SELECTOR);
      if (!imgParentEl){ return console.error('No imgParentEl'); }
      imgParentEl.innerHTML = '';

      /** @param {string} initUrl */
      async function *newPagedImgElsGen(initUrl) {
        let url = initUrl;
        /** @type {HTMLElement[]} */
        let imgEls = [];

        while (url) {
          const resp = await fetch(url, { credentials: 'same-origin' });

          url = '';
          imgEls = [];

          if (resp.ok) {
            const html = await resp.text();
            const docEl = (new DOMParser())
              .parseFromString(html, 'text/html')
              .documentElement;
            imgEls = Array.from($find(docEl, IMAGE_PARENT_SELECTOR)?.children ?? []);

            const nextEl = $find(docEl, NEXT_PAGE_SELECTOR);
            url = nextEl?.href ?? '';
          }

          yield imgEls;
        }

        return [];
      }

      const pagedImgEls = newPagedImgElsGen(location.href);

      const replaceResult = async(ob) => {
        const pagedImgElsResult = await pagedImgEls.next();
        if (pagedImgElsResult.done) {
          return ob.disconnect();
        }
        for (const imgEl of pagedImgElsResult.value) {
          if (!imgEl.classList.contains('c')) {
            imgParentEl.appendChild(imgEl);
          }
        }
      };
      let isIntersecting = false;
      const ob = new IntersectionObserver(async(entries) => {
        isIntersecting = entries[0].isIntersecting;
        if (isIntersecting) {
          do {
            await replaceResult(ob);
            await sleep(300);
          } while (isIntersecting);
        }
      });
      ob.observe($('table.ptb'));
    })();
  }
  // #endregion More Thumbs

  // #region Page Enlargement
  if (uhpConfig.pe) {
    (async() => {
      if (! $('input[name="f_search"]')) { return; }
      if (! $('.itg')) { return; }

      const isTableLayout = Boolean($('table.itg'));

      const NEXT_PAGE_SELECTOR = '.ptt td:last-child > a, .searchnav a[href*="next="]';
      const IMAGE_PARENT_SELECTOR = isTableLayout ? 'table.itg > tbody' : 'div.itg';

      const imgParentEl = $(IMAGE_PARENT_SELECTOR);
      if (!imgParentEl){ return console.error('No imgParentEl'); }
      imgParentEl.innerHTML = '';

      const statusEl = $el('h1', { textContent: 'Loading...', id: '🔓-status' });
      $('table.ptb, .itg + .searchnav, #favform + .searchnav').replaceWith(statusEl);

      /** @param {string} initUrl */
      async function *newPagedImgElsGen(initUrl) {
        let url = initUrl;
        /** @type {HTMLElement[]} */
        let imgEls = [];

        while (url) {
          const resp = await fetch(url, { credentials: 'same-origin' });

          url = '';
          imgEls = [];

          if (resp.ok) {
            const html = await resp.text();
            const docEl = (new DOMParser())
              .parseFromString(html, 'text/html')
              .documentElement;
            imgEls = Array.from($find(docEl, IMAGE_PARENT_SELECTOR)?.children ?? []);

            const nextEl = $find(docEl, NEXT_PAGE_SELECTOR);
            url = nextEl?.href ?? '';
          }

          yield imgEls;
        }

        return [];
      }

      const pagedImgEls = newPagedImgElsGen(location.href);
      const replaceResult = async(ob) => {
        const pagedImgElsResult = await pagedImgEls.next();
        if (pagedImgElsResult.done) {
          statusEl.textContent = 'End';
          return ob.disconnect();
        }
        for (const imgEl of pagedImgElsResult.value) {
          imgParentEl.appendChild(imgEl);
        }
      };
      let isIntersecting = false;
      const ob = new IntersectionObserver(async(entries) => {
        isIntersecting = entries[0].isIntersecting;
        if (isIntersecting) {
          do {
            await replaceResult(ob);
            await sleep(300);
          } while (isIntersecting);
        }
      });
      ob.observe(statusEl);
    })();
  }
  // #endregion Page Enlargement

  // #region Full Width

  if (uhpConfig.fw) {
    document.body.classList.add('🔓-full-width');
  }

  // #endregion Full Width

  // #region ubp dialog setup

  const uhpDialogEl = $el('dialog', { id: '🔓-dialog' });
  uhpDialogEl.className = (location.host === 'exhentai.org') ? 'dark' : '';
  uhpDialogEl.innerHTML = `
  <fieldset>
    <legend>Unlock Hath Perks</legend>
    <div role="group">

      <div class="option-grid">
        <label class="material-switch">
          <input type="checkbox" id="🔓-conf-abg" value="abg" />
        </label>
        <span class="🔓-conf-title">Ads-Be-Gone</span>
        <span class="🔓-conf-desc">Remove ads. You can use it with adblock webextensions.</span>
      </div>

      <div class="option-grid">
        <label class="material-switch">
          <input type="checkbox" id="🔓-conf-mt" value="mt" />
        </label>
        <span class="🔓-conf-title">More Thumbs</span>
        <span class="🔓-conf-desc">Scroll infinitely in gallery pages.</span>
      </div>

      <div class="option-grid">
        <label class="material-switch">
          <input type="checkbox" id="🔓-conf-pe" value="pe" />
        </label>
        <span class="🔓-conf-title">Page Enlargement</span>
        <span class="🔓-conf-desc">Scroll infinitely in search results pages.</span>
      </div>

      <div class="option-grid">
        <label class="material-switch">
          <input type="checkbox" id="🔓-conf-fw" value="fw" />
        </label>
        <span class="🔓-conf-title">Full Width</span>
        <span class="🔓-conf-desc">Utilize your monitor.</span>
      </div>

    </div>
  </fieldset>
`;
  uhpDialogEl.onclick = (evt) => {
    if (evt.target === uhpDialogEl) {
      uhpDialogEl.close();
      if (uhpDialogEl.dataset.hasChanged) {
        location.reload();
      }
    }
  };
  document.body.appendChild(uhpDialogEl);

  /** @type {HTMLInputElement[]} */
  const checkboxEls = $$('dialog#🔓-dialog input[type="checkbox"]');
  for (const checkboxEl of checkboxEls) {
    checkboxEl.checked = uhpConfig[checkboxEl.value];
    checkboxEl.onchange = () => {
      uhpConfig[checkboxEl.value] = checkboxEl.checked;
      uhpDialogEl.dataset.hasChanged = true;
    };
  }

  const nb = $('#nb');
  nb.appendChild(
    $html(`
    <div>
      <a id="🔓-entry" href="javascript:;">Unlock Hath Perks</a>
    </div>
  `),
  );

  $('a#🔓-entry').onclick = () => uhpDialogEl.showModal();
  // #endregion ubp dialog setup

  // #region override e-h style

  $style(`
/* nav bar */
#nb {
  width: initial;
  max-width: initial;
  max-height: initial;
  justify-content: center;
}

/* search input */
table.itc + p.nopm {
  display: flex;
  flex-flow: row wrap;
  justify-content: center;
}
input[name="f_search"] {
  width: 100%;
}

/* /favorites.php */
input[name="favcat"] + div {
  display: flex;
  flex-flow: row wrap;
  justify-content: center;
  gap: 8px;
}

/* gallery grid */
.gl1t {
  display: flex;
  flex-flow: column;
}
.gl1t > .gl3t {
  flex: 1;
}
.gl1t > .gl3t > a {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}`);

  // #endregion override e-h style

  // #region uhp style

  $style(`
#🔓-status {
  text-align: center;
  font-size: 3rem;
  clear: both;
  padding: 2rem 0;
}

#🔓-dialog {
  padding: 1.2rem;
  background-color: floralwhite;
  border-radius: 1rem;
  font-size: 1.4rem;
  color: darkred;
  max-width: 950px;

  &.dark {
    background-color: dimgray;
    color: ghostwhite;
  }

  fieldset > legend {
    font-size: 2rem;
  }

  .option-grid {
    display: grid;
    grid-template-columns: max-content 14rem 1fr;
    column-gap: 1rem;
    padding: 0.5rem 1rem;
    align-items: center;
  }
}

.🔓-full-width :where(#gdt, div.ido) {
  max-width: initial !important;
  margin: 1rem !important;
}

@supports (display:grid) {
  .🔓-full-width .gld {
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 0.5rem;
  }
}

/* Modified https://bootsnipp.com/snippets/featured/material-design-switch */
label.material-switch > input[type="checkbox"] {
  display: none;
}

label.material-switch {
  display: inline-block;
  position: relative;
  margin: 6px;
  border-radius: 8px;
  width: 40px;
  height: 16px;
  opacity: 0.3;
  background-color: rgb(0, 0, 0);
  box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5);
  transition: all 0.4s ease-in-out;
  cursor: pointer;
}

label.material-switch::after {
  position: absolute;
  top: -4px;
  left: -4px;
  border-radius: 16px;
  width: 24px;
  height: 24px;
  content: "";
  background-color: rgb(255, 255, 255);
  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
  transition: all 0.3s ease-in-out;
}

label.material-switch:has(> input[type="checkbox"]:checked) {
  background-color: #0e0;
  opacity: 0.7;
}

label.material-switch:has(> input[type="checkbox"]:checked)::after {
  background-color: inherit;
  left: 20px;
}`);

  // #endregion uhp style

})();