Advanced RoyalRoad

Makes RoyalRoad search infinitely scrollable and adds a minimum number of chapters filter

// ==UserScript==
// @name         Advanced RoyalRoad
// @namespace    https://github.com/RedCommander735
// @version      0.1.1
// @description  Makes RoyalRoad search infinitely scrollable and adds a minimum number of chapters filter
// @author       RedCommander735
// @license      GNU GPLv3
// @match        https://www.royalroad.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=royalroad.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      www.royalroad.com
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
.arr-checkbox {
  webkit-transition:all .3s;
  cursor:pointer;
  display:inline-block;
  font-size:14px;
  margin-bottom:15px;
  padding-left:30px;
  position:relative;
  -moz-transition:all .3s;
  -ms-transition:all .3s;
  -o-transition:all .3s;
  transition:all .3s
}
.arr-checkbox>input {
  filter:alpha(opacity=0);
  opacity:0;
  position:absolute;
  z-index:-1
}
.arr-checkbox>span {
  background:#e6e6e6;
  border:1px solid transparent;
  height:19px;
  left:0;
  position:absolute;
  top:0;
  width:19px
}
.arr-checkbox>span:after {
  content:"";
  display:none;
  position:absolute
}
.arr-checkbox:hover>input:not([disabled])~span,
.arr-checkbox>input:checked~span,
.arr-checkbox>input:focus~span {
  webkit-transition:all .3s;
  background:#d9d9d9;
  -moz-transition:all .3s;
  -ms-transition:all .3s;
  -o-transition:all .3s;
  transition:all .3s
}
.arr-checkbox>input:checked~span:after,
.mt-radio>input:checked~span:after {
  display:block
}
.arr-checkbox:hover>input:not([disabled]):checked~span,
.arr-checkbox>input:checked~span {
  webkit-transition:all .3s;
  background:#d9d9d9;
  -moz-transition:all .3s;
  -ms-transition:all .3s;
  -o-transition:all .3s;
  transition:all .3s
}
.arr-checkbox>span:after {
  border:solid #666;
  border-width:0 2px 2px 0;
  height:10px;
  left:6px;
  top:3px;
  transform:rotate(45deg);
  width:5px
}
    `)


    let min_chapters;
    let current_page;
    let infiniscroll;




    function insertAfterElement(element, toInsert) {
        element.parentNode.insertBefore(toInsert, element.nextSibling);
    }

    function isElementInViewport(el) {

        if (el.style.display == "none") {
            return false;
        }

        var rect = el.getBoundingClientRect();

        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
        );
    }

    function isAnyInViewport(elements) {
        let isVisible = false;
        elements.forEach((elem) => {
            if (isElementInViewport(elem)) {isVisible = true;}
        })

        return isVisible;
    }

    async function get_next_search_page(url, page_num) {
        console.log(`Advanced RoyalRoad Search: [InfiniScroll] Loading next page: ${page_num}`)

        current_page = page_num;
        const DomParser = new DOMParser();

        const next_page = await GM.xmlHttpRequest({ url: url })
                    .then(resp => resp.responseText)
                    .catch(e => console.error(e));

        const next_page_dom = DomParser.parseFromString(next_page, 'text/html')
        let search_container = next_page_dom.querySelector('.search-container')

        let fiction_list = search_container.querySelector('.fiction-list')

        let fictions = fiction_list.children


        for (let i = 0; i < fictions.length; i++) {
            let element = fictions[i];
            const chapters = parseInt(element.querySelector('div.row.stats > div:nth-child(5) > span').textContent.split(' ')[0].replaceAll(',', ''), 10);
            if (chapters < parseInt(min_chapters, 10)) {element.setAttribute("style", "display: none;");}
        };

        document.querySelector('div.col-md-8:nth-child(1)').appendChild(search_container)
        document.querySelector('.search-container:nth-last-child(2)').querySelector('div.text-center').setAttribute("style", "display: none;");
    }


    function getCookie(name) {
        const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
        if (match) return match[2];
    }

    let theme = getCookie('rrl_style')
    if (theme == 'dark') {
        GM_addStyle(`
        .arr-checkbox span {
	        border-color: hsla(0,0%,39%,.8) !important;
        }
        `)
    }


    const urlParams = new URLSearchParams(window.location.search)
    try {
        current_page = urlParams.get('page')
    } catch {
        current_page = 1;
    }

    try {
        min_chapters = urlParams.get('minChapters')
    } catch {
        min_chapters = 0;
    }

    try {
        infiniscroll = urlParams.get('infiniscroll')
    } catch {
        infiniscroll = 'false';
    }

    const min_chapters_html = `<div class="form-group">
                                   <label>Minimum Chapter Count</label>
                                   <input type="number" step="1" class="text-center col-xs-4 col-md-3 col-lg-2" name="minChapters" id="minChapters" value="${min_chapters}" style="margin-bottom: 15px">
                               </div>`

    const scroll_html = `<div class="form-group">
                             <label class="arr-checkbox" id="infiniscroll_label">
                                 <input type="text" name="infiniscroll" id="infiniscroll_text" style="display: none;" value="${infiniscroll}" />
                                 <input id="infiniscroll" value="true" type="checkbox"> Infinite scrolling
                                 <span></span>
                             </label>
                         </div>`

    const DomParser = new DOMParser();
    const scroll_dom = DomParser.parseFromString(scroll_html, 'text/html');
    const scroll = scroll_dom.querySelector('div.form-group');
    const scroll_label = scroll.querySelector('#infiniscroll_label')
    const scroll_text = scroll.querySelector('#infiniscroll_text')
    const scroll_checkbox = scroll.querySelector('#infiniscroll')
    const rating = document.querySelector('div.form-group:nth-child(15)');

    const chapter_dom = DomParser.parseFromString(min_chapters_html, 'text/html');
    const chapter = chapter_dom.querySelector('div.form-group');
    insertAfterElement(rating, scroll);
    insertAfterElement(scroll, chapter);

    scroll_checkbox.checked = (infiniscroll == 'true');

    scroll_label.addEventListener('click', (event) => {
        event.preventDefault()
        event.stopPropagation()
        let toggle = scroll_checkbox.checked;
        if (toggle) {
            scroll_checkbox.checked = false
            scroll_text.value = "false"
        } else {
            scroll_checkbox.checked = true
            scroll_text.value = "true"
        }
    })

    let search_container = document.querySelector('.search-container')

    let fiction_list = search_container.querySelector('.fiction-list')

    let fictions = fiction_list.children


    for (let i = 0; i < fictions.length; i++) {
        let element = fictions[i];
        const chapters = parseInt(element.querySelector('div.row.stats > div:nth-child(5) > span').textContent.split(' ')[0].replaceAll(',', ''), 10);
        if (chapters < parseInt(min_chapters, 10)) {element.setAttribute("style", "display: none;");}
    };


    onscroll = (event) => {
        if ((isAnyInViewport(document.querySelector('.search-container:last-child').querySelectorAll('div.row.fiction-list-item:nth-last-child(-n+5)')) || isElementInViewport(document.querySelector('.search-container:last-child > .text-center'))) && (infiniscroll == 'true')) {
            if (window.location.pathname == '/fictions/search') {
                let next_nav;
                let last_nav;
                let page_navs = document.querySelectorAll('.pagination > li')

                page_navs.forEach((element) => {
                    if (element.childNodes[0].innerText.startsWith('Next')) {
                        next_nav = element.childNodes[0];
                    }
                    if (element.childNodes[0].innerText.startsWith('Last')) {
                        last_nav = element.childNodes[0];
                    }
                });


                let next_page_link;
                if (next_nav) {
                    next_page_link = next_nav.href;
                } else if (last_nav) {
                    next_page_link = last_nav.href;
                }
                if (next_page_link) {
                    let next_page_number = new URL(next_page_link).searchParams.get('page');
                    if (next_page_number != current_page) {
                        get_next_search_page(next_page_link, next_page_number);
                    }
                }
            }
        }
    };

})();