mataamad / rrl-hide-improved

// ==UserScript==
// @name         rrl-hide-improved
// @namespace    mataamad
// @version      0.3
// @description  allows hiding of royal road fictions, reducing their visibility, and enables infinite scroll
// @author       fsoc, mataamad
// @license      MIT
// @copyright    2019, lw (https://fsoc.space), mataamad
// @match        https://www.royalroad.com/fictions/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

// ==OpenUserJS==
// @author mataamad
// ==/OpenUserJS==

(function () {

    // medium priority
    //  the button states aren't intuitive - use a checkbox or something
    // add 'maybehide'
    // tidy up the code a bit

    // allow defining a couple of tags (read / dropped / sounds bad / hiatus etc.) and allow enabling or disabling the tags
    // at the moment if I hide something I might never see it again

    // low priority:
    // support the search page
    // style 'loading' indicator
    // can use tapermonkey 'fetch' instead of XMLHttpRequest for simplicity I think
    // add number next to stars because it's hard to tell the star amounts apart
    // add the ability to add notes to fictions

    const styles = document.createElement('style');
    styles.innerHTML = `
    .fiction-list-item.rr-hider-ignore div div {
      display: none;
    }

    .fiction-list-item.rr-hider-ignore figure {
      display: none;
    }

    .fiction-list-item.rr-hider-ignore {
      padding: 3px 0;
    }

    .fiction-list-item.rr-hider-ignore .fiction-title {
      margin: 0;
    }
    .fiction-list-item.rr-hider-hide-title {
      display: none;
    }

    .rr-hider-hide {
      display: none;
    }

    .rr-hider-full-hide-button {
      margin-bottom: 10px;
    }

    .rr-hider-full-hide-button.off {
      background-color: #444 !important;
      color: #777 !important;
      border: 1px solid #444 !important;
    }

    .rr-hider-full-hide-button.enabled {
    }

    .rr-hider-autoload {
      margin-left: 8px !important;
    }

    .rr-hider-sort-loaded {
      margin-left: 8px !important;
    }

    .rr-hider-autoload.off {
      background-color: #444 !important;
      color: #777 !important;
      border: 1px solid #444 !important;
    }

    .rr-hider-autoload.enabled {
    }

    .rr-hider-page-number-indicator {
      border-bottom: 1px solid hsla(0,0%,100%,.1)
    }

    .rr-hider-loading {

    }

    .fiction-list-item.rr-hider-ignore .rr-hider-hide-fiction {
      display: none;
    }

    .fiction-list-item.rr-hider-show .rr-hider-show-fiction {
      display: none;
    }

    `;
    document.head.appendChild(styles);

    // const { localStorage } = window;
    const localStorage = {
      getItem: GM_getValue,
      setItem: GM_setValue,
    }

    /*for (key of Object.keys(window.localStorage).filter(key => key.startsWith('ign_')).filter(key => window.localStorage[key] === "true")) {
      localStorage.setItem(key, "true");
    }*/

    let showHiddenTitles = localStorage.getItem('rr-hider-show-hidden-titles', "true") === "true";
    let autoLoad = localStorage.getItem('rr-hider-auto-load', "true") === "true";
    let currentPage = 1;
    let loading = false;

    const setClass = (element, cssClass, enable = true) => {
      if (enable) {
        element.classList.add(cssClass);
      }
      else {
        element.classList.remove(cssClass);
      }
    };

    const updateHideStatuses = () => {
      const fictions = document.querySelectorAll('.fiction-list-item');
      for (const fiction of fictions) {

        const id = "ign_" + fiction.getElementsByClassName('fiction-title').item(0).getElementsByTagName('a')[0].getAttribute('href').split('/')[2];
        const ignore = localStorage.getItem(id) === "true";

        setClass(fiction, 'rr-hider-show', !ignore);
        setClass(fiction, 'rr-hider-ignore', ignore);
        setClass(fiction, 'rr-hider-hide-title', ignore && !showHiddenTitles);
      }
    }

    const addHideButtons = (fictions) => {
      for (const fiction of fictions) {

        const id = "ign_" + fiction.getElementsByClassName('fiction-title').item(0).getElementsByTagName('a')[0]
          .getAttribute('href').split('/')[2];

        const hideLink = document.createElement('a');
        hideLink.setAttribute('class', 'rr-hider-hide-fiction font-red-sunglo');
        hideLink.insertAdjacentText('afterbegin', 'Hide');

        const showLink = document.createElement('a');
        showLink.setAttribute('class', 'rr-hider-show-fiction font-red-sunglo');
        showLink.insertAdjacentText('afterbegin', 'Show');

        hideLink.onclick = () => {
          const status = localStorage.getItem(id) === "true";
          localStorage.setItem(id, (!status).toString());

          updateHideStatuses();
        };
        showLink.onclick = hideLink.onclick;

        const container = document.createElement('small');
        container.append(hideLink);
        container.append(showLink);

        const heading = fiction.getElementsByClassName('fiction-title').item(0);
        heading.append(container);
      }
    }

    const sortLoadedByRating = () => {
      const fictionList = document.querySelector('.fiction-list')
      const fictions = fictionList.querySelectorAll('.fiction-list-item');

      /*for (var fiction of fictions) {
        fictionList.removeChild(fiction);
      }*/

      const getRating = (fiction) => parseFloat(fiction.querySelector('.star').getAttribute('title'));
      const sorted = [...fictions].sort((a, b) => {
        return  getRating(b) - getRating(a);
      });

      fictionList.append(...sorted);
    };

    const fictionList = document.querySelector('.fiction-list');
    if (!fictionList) {
      return;
    }

    const fictions = document.querySelectorAll('.fiction-list-item');
    addHideButtons(fictions);
    updateHideStatuses();

    const loadingElement = document.createElement('div');
    loadingElement.insertAdjacentText('afterbegin', 'loading more stories...');
    loadingElement.setAttribute('class', 'rr-hider-loading font-red-sunglo');
    fictionList.parentNode.insertBefore(loadingElement, fictionList.nextSibling);

    const userscriptButtons = document.createElement('div');
    userscriptButtons.setAttribute('class', 'btn-group');

    const fullHide = document.createElement('a');
    fullHide.setAttribute('class', 'rr-hider-full-hide-button btn blue-dark');
    fullHide.insertAdjacentText('afterbegin', 'Fully Hide Ignored');
    setClass(fullHide, 'enabled', !showHiddenTitles);
    setClass(fullHide, 'off', showHiddenTitles);
    fullHide.onclick = () => {
      showHiddenTitles = !showHiddenTitles;
      localStorage.setItem('rr-hider-show-hidden-titles', showHiddenTitles.toString())
      setClass(fullHide, 'enabled', !showHiddenTitles);
      setClass(fullHide, 'off', showHiddenTitles);
      updateHideStatuses();
    }
    userscriptButtons.appendChild(fullHide);


    fictionList.parentNode.insertBefore(userscriptButtons, fictionList);

    const currentLocation = window.location.href;

    let pagingUrl = '';
    const validLazyLoadUrls = [
      'https://www.royalroad.com/fictions/active-popular',
      'https://www.royalroad.com/fictions/best-rated',
      'https://www.royalroad.com/fictions/complete',
      'https://www.royalroad.com/fictions/weekly-popular',
      'https://www.royalroad.com/fictionsr/latest-updates',
      'https://www.royalroad.com/fictionsr/new-releases',
      // https://www.royalroad.com/fictions/search, // to allow lazy loading search I'd need to preserver the URL parameters
      // 'https://www.royalroad.com/fictions/trending', // trending is limited to 50 pages
    ];
    if (validLazyLoadUrls.includes(currentLocation)) {
      pagingUrl = currentLocation + '?page=';
    }

    if (pagingUrl) {
      const autoLoadElement = document.createElement('a');
      autoLoadElement.setAttribute('class', 'rr-hider-autoload btn blue-dark');
      autoLoadElement.insertAdjacentText('afterbegin', 'Infinite Scroll');
      setClass(autoLoadElement, 'enabled', autoLoad);
      setClass(autoLoadElement, 'off', !autoLoad);
      autoLoadElement.onclick = () => {
        autoLoad = !autoLoad;
        localStorage.setItem('rr-hider-auto-load', autoLoad.toString())
        setClass(autoLoadElement, 'enabled', autoLoad);
        setClass(autoLoadElement, 'off', !autoLoad);
        updateHideStatuses();
        updateShowPagination();
      }
      userscriptButtons.appendChild(autoLoadElement);
    }

    const sortLoaded = document.createElement('a');
    sortLoaded.setAttribute('class', 'rr-hider-sort-loaded btn blue-dark');
    sortLoaded.insertAdjacentText('afterbegin', 'Sort Loaded By Rating');
    sortLoaded.onclick = sortLoadedByRating;
    userscriptButtons.appendChild(sortLoaded);

    const updateLoadingIndicator = () => {
      setClass(loadingElement, 'rr-hider-hide', !loading);
    }

    const updateShowPagination = () => {
      const pagination = document.querySelector('.pagination');
      if (pagination) {
        setClass(pagination, 'rr-hider-hide', autoLoad && pagingUrl);
      }
    }

    updateLoadingIndicator();
    updateShowPagination();

    const tryLoadNextPage = () => {
      const closeToEnd = window.scrollY + window.innerHeight * 1.5 > fictionList.offsetTop + fictionList.clientHeight;
      if (loading || !autoLoad || !closeToEnd || !pagingUrl) {
        return;
      }
      loading = true;
      currentPage++;
      updateLoadingIndicator();
      let xhr = new XMLHttpRequest();

      xhr.open('GET', pagingUrl + currentPage);
      xhr.onload = function () {
        const fictionList = document.querySelector('.fiction-list')
        var pageNumberIndicator = document.createElement('h1');

        pageNumberIndicator.insertAdjacentText('afterbegin', 'Page ' + currentPage);
        pageNumberIndicator.setAttribute('class', 'bold uppercase font-red-sunglo rr-hider-page-number-indicator')
        fictionList.appendChild(pageNumberIndicator);

        var res = document.createElement('div');
        res.insertAdjacentHTML('afterbegin', xhr.responseText);
        var nextPageFictions = res.querySelectorAll('.fiction-list .fiction-list-item')

        addHideButtons(nextPageFictions);

        fictionList.append(...nextPageFictions);

        updateHideStatuses();

        loading = false;
        updateLoadingIndicator();
        tryLoadNextPage();
      }
      xhr.send();
    }

    window.addEventListener('scroll', tryLoadNextPage);
    tryLoadNextPage();

  })();