E-Hentai - UX Tweaks

Numerous features to enrich your browsing experience

// ==UserScript==
// @name         E-Hentai - UX Tweaks
// @namespace    brazenvoid
// @version      1.3.11
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Numerous features to enrich your browsing experience
// @match        https://e-hentai.org/*
// @match        https://exhentai.org/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @require      https://update.greasyfork.org/scripts/375557/1244990/Base%20Brazen%20Resource.js
// @require      https://update.greasyfork.org/scripts/416104/1392660/Brazen%20UI%20Generator.js
// @require      https://update.greasyfork.org/scripts/418665/1245040/Brazen%20Configuration%20Manager.js
// @require      https://update.greasyfork.org/scripts/429587/1244644/Brazen%20Item%20Attributes%20Resolver.js
// @require      https://update.greasyfork.org/scripts/416105/1384192/Brazen%20Base%20Search%20Enhancer.js
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

GM_addStyle(
    `#settings-wrapper{min-width:310px;width:310px}.disliked-tag{background-color:lightcoral !important;color:white !important}.disliked-tag:hover{background-color:indianred !important}.disliked-tag > a{color:white !important}.disliked-tag.favourite-tag{background-color:orange !important}.disliked-tag.favourite-tag:hover{background-color:darkorange !important}.favourite-tag{background-color:mediumseagreen !important;color:white !important}.favourite-tag:hover{background-color:forestgreen !important}.favourite-tag > a{color:white !important}`);

const IS_GALLERY_PAGE = $('#gdt').length;
const IS_SEARCH_PAGE = $('#f_search').length;
const IS_SMALL_WINDOW = $('.stuffbox').length;
const IS_TAG_SEARCH_PAGE = window.location.pathname.startsWith('/tag');
const IS_UPLOADER_SEARCH_PAGE = window.location.pathname.startsWith('/uploader');

const UI_DEFAULTS_PAGE_RANGE = 'Page Range';
const UI_DEFAULTS_PAGE_RANGE_ENABLE = 'Enable Page Range Filter';
const UI_DEFAULTS_RATING = 'Rating';
const UI_DEFAULTS_RATING_ENABLE = 'Enable Rating Filter';
const UI_DEFAULTS_TAGS = 'Tags';
const UI_DEFAULTS_TAGS_ENABLE = 'Enable Default Tags';

const UI_FAVOURITE_TAGS = 'Favourite Tags';
const UI_DISLIKED_TAGS = 'Disliked Tags';
const UI_VISITED_HIGHLIGHT = 'Highlight Visited';
const UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES = 'Auto Next Page';

class EHentaiSearchAndUITweaks extends BrazenBaseSearchEnhancer
{
  constructor()
  {
    super({
      isUserLoggedIn: false,
      itemDeepAnalysisSelector: '',
      itemLinkSelector: 'td.gl3m.glname > a, td.gl3c.glname > a, div.gl2e > div > a, a',
      itemListSelectors: 'table.itg, div.itg',
      itemNameSelector: 'td.gl3m.glname > a > div.glink, td.gl3c.glname > a > div.glink, div.gl4e.glname > div.glink, div.gl4t.glname.glink',
      itemSelectors: 'td.gl2e, div.gl1t',
      requestDelay: 0,
      scriptPrefix: 'e-hentai-ux-',
      tagSelectorGenerator: (tag) => {
        tag = tag.trim();
        if (IS_GALLERY_PAGE) {
          let tagAttribute = tag.replaceAll(' ', '_')
          return 'div[id="td_' + tagAttribute + '"], a[id="ta_' + tagAttribute + '"]';
        }
        return 'div.gt[title="' + tag + '"], div.gtl[title="' + tag + '"]';
      },
    });
    this._setupFeatures();
    this._setupUI();
    this._setupEvents();
  }

  /**
   * @param {string} tag
   * @return {string}
   * @private
   */
  _formatTag(tag)
  {
    if (tag.includes(':') && !tag.includes('"') && (tag.includes(' ') || tag.includes('+'))) {
      tag = tag.replace(':', ':"') + '"';
    }
    return tag;
  }

  /**
   * @param {{}} range
   * @param {URLSearchParams} queryParams
   * @private
   */
  _handleDefaultPageRangeFilter(range, queryParams)
  {
    if (range.minimum > 0) {
      queryParams.set('f_spf', range.minimum);
    }
    if (range.maximum > 0) {
      queryParams.set('f_spf', range.maximum);
    }
  }

  /**
   * @param {string} rating
   * @param {URLSearchParams} queryParams
   * @private
   */
  _handleDefaultRatingsFilter(rating, queryParams)
  {
    queryParams.set('f_srdd', rating);
  }

  /**
   * @param {string[]} tags
   * @param {URLSearchParams} queryParams
   * @private
   */
  _handleDefaultTags(tags, queryParams)
  {
    let existingTags = queryParams.get('f_search');
    let updatedTags = existingTags;
    let include = true;

    for (let tag of tags) {
      if (!existingTags.includes(tag)) {
        updatedTags += '+' + this._formatTag(tag);
      } else {
        include = false;
        break;
      }
    }

    if (include) {
      queryParams.set('f_search', updatedTags);
    }
  }

  /**
   * @private
   */
  _handleDefaults()
  {
    let queryParams = new URLSearchParams(window.location.search);
    let existingParams = queryParams.toString();

    if (!queryParams.has('next') &&
        (this._getConfig(UI_DEFAULTS_PAGE_RANGE_ENABLE) || this._getConfig(UI_DEFAULTS_RATING_ENABLE) ||
            this._getConfig(UI_DEFAULTS_TAGS_ENABLE))) {

      if (!queryParams.has('f_search')) {

        let existingTag = [];
        let urlSegments = window.location.pathname.split('/')

        if (IS_TAG_SEARCH_PAGE) {
          existingTag = urlSegments.pop().trim();
        } else if (IS_UPLOADER_SEARCH_PAGE) {
          existingTag = 'uploader:' + urlSegments.pop().trim();
        }
        queryParams.set('f_search', existingTag.length ? this._formatTag(existingTag) : '');
      }

      if (!queryParams.has('advsearch')) {
        queryParams.set('advsearch', '1');
      }

      let validatePageRange = (range, defaultValidator) => defaultValidator(range) && !queryParams.has('f_spf') &&
          !queryParams.has('f_spt');

      this._performTogglableComplexOperation(UI_DEFAULTS_PAGE_RANGE_ENABLE, UI_DEFAULTS_PAGE_RANGE, validatePageRange,
          (range) => {
            this._handleDefaultPageRangeFilter(range, queryParams);
          });

      let validateRatingFilter = (range, defaultValidator) => defaultValidator(range) && !queryParams.has('f_srdd');

      this._performTogglableComplexOperation(UI_DEFAULTS_RATING_ENABLE, UI_DEFAULTS_RATING, validateRatingFilter,
          (rating) => {
            this._handleDefaultRatingsFilter(rating, queryParams);
          });

      this._performTogglableOperation(UI_DEFAULTS_TAGS_ENABLE, UI_DEFAULTS_TAGS, (tags) => {
        this._handleDefaultTags(tags, queryParams);
      });

      let updatedParams = queryParams.toString().replaceAll('%2B', '+');
      if (updatedParams !== existingParams) {
        if (IS_TAG_SEARCH_PAGE || IS_UPLOADER_SEARCH_PAGE) {
          window.location = window.location.origin + '?' + updatedParams;
        } else {
          window.location = window.location.origin + window.location.pathname + '?' + updatedParams;
        }
      }
    }
  }

  /**
   * @private
   */
  _handleOpenGalleryImages()
  {
    let images = $('div.gdtl a > img, div.gdtm a > img');
    let firstPage = images.first().attr('alt');
    let firstPageNumber = Number(firstPage);
    let paddedPageLength = firstPage.length;
    let maxPages = firstPageNumber + images.length - 1;

    for (let page = maxPages; page >= firstPageNumber; page--) {

      let paddedPage = page.toString().padStart(paddedPageLength, '0');
      window.open(images.filter('[alt="' + paddedPage + '"]').parent().attr('href'));
    }

    if (this._getConfig(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES)) {

      let page = window.location.href.split('=')[1] || 0;
      let pageNavs = $('.ptt td');
      let maxPages = Number.parseInt(pageNavs.eq(pageNavs.length - 2).children('a').text()) - 1;
      if (page < maxPages) {

        let uri = window.location.href;
        if (page === 0) {
          uri += '?p=1';
        } else {
          uri = uri.replace('?p=' + page++, '?p=' + page);
        }
        window.location = uri;
      }
    }
  }

  /**
   * @private
   */
  _setupEvents()
  {
    this._onValidateInit = () => !IS_SMALL_WINDOW;

    this._onUIBuild(() => {
      this._performOperation(UI_VISITED_HIGHLIGHT, () => {
        GM_addStyle(`td.gl2e > div > a:visited > .glname > .glink {color: black;}`);
      });
      if (IS_SEARCH_PAGE) {
        this._handleDefaults();
      }
    });

    this._onUIBuilt(() => this._uiGen.getSelectedSection()[0].userScript = this);

    this._onItemHide = (item) => {
      if (item.is('td.gl2e')) {
        item.parent().addClass('noncompliant-item');
        item.parent().hide();
      } else {
        item.removeClass('noncompliant-item');
        item.hide();
      }
    };

    this._onItemShow = (item) => {
      if (item.is('td.gl2e')) {
        item.parent().removeClass('noncompliant-item');
        item.parent().show();
      } else {
        item.removeClass('noncompliant-item');
        item.show();
      }
    };
  }

  /**
   * @private
   */
  _setupFeatures()
  {
    this._configurationManager.
        addFlagField(
            UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES, 'Automatically navigates to the next page after opening all images.').
        addFlagField(
            UI_VISITED_HIGHLIGHT, 'Colours the visited gallery links black, to make them distinct.').
        addRangeField(
            UI_DEFAULTS_PAGE_RANGE, 0, 2000, 'Enable default page range filter in searches.').
        addFlagField(
            UI_DEFAULTS_PAGE_RANGE_ENABLE,
            'Always set these page limits in searches. Ignored if you set your own values on the page.').
        addRadiosGroup(
            UI_DEFAULTS_RATING,
            [
              ['2 stars', '2'],
              ['3 stars', '3'],
              ['4 stars', '4'],
              ['5 stars', '5'],
            ],
            'Always set this rating filter in searches. Ignored if you set your own value on the page.').
        addFlagField(
            UI_DEFAULTS_RATING_ENABLE, 'Enable default rating filter in searches').
        addRulesetField(
            UI_DEFAULTS_TAGS,
            3,
            'Always add the following tags in search. Can be overridden with at least one tag present.').
        addFlagField(
            UI_DEFAULTS_TAGS_ENABLE, 'Enable default tags in searches.');

    let otherTagSections = IS_GALLERY_PAGE ? $('#taglist') : null;

    this._addItemTagHighlights(
        UI_FAVOURITE_TAGS, otherTagSections, 'favourite-tag', 'Specify favourite tags to highlight.', 10);
    this._addItemTagHighlights(
        UI_DISLIKED_TAGS, otherTagSections, 'disliked-tag', 'Specify disliked tags to highlight.', 10);
    this._addItemTagBlacklistFilter(20);
  }

  /**
   * @private
   */
  _setupUI()
  {
    let galleryOptions = [];
    if (IS_GALLERY_PAGE) {

      galleryOptions = [
        this._uiGen.createSeparator(),
        this._uiGen.createFormButton(
            'Open Gallery Images',
            'Opens all images on current page of this gallery.',
            () => this._handleOpenGalleryImages()
        ),
      ];
    }

    this._userInterface = [
      this._uiGen.createTabsSection(['Filters', 'Highlights', 'Defaults', 'Global'], [
        this._uiGen.createTabPanel('Filters', true).append([
          this._configurationManager.createElement(OPTION_ENABLE_TAG_BLACKLIST),
          this._configurationManager.createElement(FILTER_TAG_BLACKLIST),
        ]),
        this._uiGen.createTabPanel('Highlights').append([
          this._configurationManager.createElement(UI_FAVOURITE_TAGS),
          this._configurationManager.createElement(UI_DISLIKED_TAGS),
        ]),
        this._uiGen.createTabPanel('Defaults').append([
          this._configurationManager.createElement(UI_DEFAULTS_PAGE_RANGE_ENABLE),
          this._configurationManager.createElement(UI_DEFAULTS_PAGE_RANGE),
          this._uiGen.createSeparator(),
          this._configurationManager.createElement(UI_DEFAULTS_RATING),
          this._uiGen.createBreakSeparator(),
          this._configurationManager.createElement(UI_DEFAULTS_RATING_ENABLE),
          this._uiGen.createSeparator(),
          this._configurationManager.createElement(UI_DEFAULTS_TAGS_ENABLE),
          this._configurationManager.createElement(UI_DEFAULTS_TAGS),
        ]),
        this._uiGen.createTabPanel('Global').append([
          this._configurationManager.createElement(UI_AUTO_NEXT_ON_OPEN_ALL_IMAGES),
          this._configurationManager.createElement(UI_VISITED_HIGHLIGHT),
          this._configurationManager.createElement(OPTION_ALWAYS_SHOW_SETTINGS_PANE),
          this._uiGen.createSeparator(),
          this._createSettingsBackupRestoreFormActions(),
        ]),
      ]),
      IS_GALLERY_PAGE ? '' : this._uiGen.createStatisticsFormGroup(FILTER_TAG_BLACKLIST),
      ...galleryOptions,
      this._uiGen.createSeparator(),
      this._createSettingsFormActions(),
      this._uiGen.createSeparator(),
      this._uiGen.createStatusSection(),
    ];
  }
}

(new EHentaiSearchAndUITweaks).init();