thisvid infinite scroll and filter

[s]try to take over the world![/s] infinite scroll, filter: public, private, duration

اعتبارا من 26-01-2024. شاهد أحدث إصدار.

// ==UserScript==
// @name         thisvid infinite scroll and filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2024-01-18
// @description  [s]try to take over the world![/s] infinite scroll, filter: public, private, duration
// @author       homo
// @match        https://thisvid.com/
// @match        https://thisvid.com/latest-updates/*
// @match        https://thisvid.com/tags/*
// @match        https://thisvid.com/categories/*
// @match        https://thisvid.com/*/?q=*
// @match        https://thisvid.com/members/*
// @exclude      https://thisvid.com/videos/*
// @icon         data:image/jpeg;base64,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
// @grant        GM_addStyle
// ==/UserScript==
(function () {
    'use strict';

    console.clear();
    console.log('\n\ntapermonkey-thisvidscript\n\n');

    class Utils {
        static $ = (x, e = document.body) => e.querySelector(x);
        static $$ = (x, e = document.body) => e.querySelectorAll(x);

        static findElementsBetweenIds = (startId, endId) => {
            const startElement = document.getElementById(startId);
            const endElement = document.getElementById(endId);

            if (!startElement || !endElement) {
                return [];
            }

            const elementsBetween = [];
            let currentElement = startElement.nextElementSibling;

            while (currentElement && currentElement !== endElement) {
                elementsBetween.push(currentElement);
                currentElement = currentElement.nextElementSibling;
            }

            return elementsBetween;
        };

        static parseHTML = (str) => {
            var temp = document.createElement('html');
            temp.innerHTML = str;
            return temp;
        };

        static fetchHtml = async (url) =>
        fetch(url)
        .then((r) => r.text())
        .then((d) => parseHTML(d));

        static getRandomRgb = () => {
            const n = Math.round(0xffffff * Math.random());
            return `rgb(${n >> 16},${(n >> 8) & 255},${n & 255})`;
        };

        static parseCSSUrl = (s) => s.match(/(?<=\(").*(?="\))/)[0];

        static timeToSeconds = (t) =>
        t
        .match(/\d+/gm)
        .reverse()
        .map((s, i) => parseInt(s) * Math.pow(60, i))
        .reduce((a, b) => a + b);

        static circularShift = (n, c = 6, s = 1) => (n + s) % c || c;

        static isElementInViewport = (el) => {
            var rect = el.getBoundingClientRect();
            return (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <=
                (window.innerHeight ||
                 document.documentElement
                 .clientHeight) /* or $(window).height() */ &&
                rect.right <=
                (window.innerWidth ||
                 document.documentElement.clientWidth) /* or $(window).width() */
            );
        };

        static toggleClass = (element, className, condition) => {
            if (condition) {
                element.classList.add(className);
            } else {
                element.classList.remove(className);
            }
        };

        static parseDom = (html) => {
            return new DOMParser().parseFromString(html, 'text/html').body.firstChild;
        }
    }

    const {
        $,
        $$,
        findElementsBetweenIds,
        fetchHtml,
        parseHTML,
        parseCSSUrl,
        circularShift,
        isElementInViewport,
        timeToSeconds,
        getRandomRgb,
        toggleClass,
        parseDom
    } = Utils;

    class Tick {
        constructor(interval) {
            this.interval = interval;
        }

        start(callback) {
            if (this.ticker) {
                this.stop();
            }
            callback();
            this.ticker = setInterval(callback, this.interval);
        }

        stop() {
            clearInterval(this.ticker);
            this.ticker = false;
        }
    }

    class QueueDuplicatesFilter {
        constructor(queueLimit = 120) {
            this.queueLimit = queueLimit;
            this.queue = [];
        }

        put(item) {
            const isUnique = !this.queue.includes(item);
            if (isUnique) {
                if (this.queue.length > this.queueLimit) {
                    this.queue.shift();
                }
                this.queue.push(item);
            }
            return isUnique;
        }
    }

    class ReactiveLocalStorage {
        constructor(data) {
            if (data) {
                Object.assign(this, data);
                this.observeProps(this);
            }
        }

        getLS(prop) {
            const val = localStorage.getItem(prop);
            if (val !== null) {
                return JSON.parse(val);
            } else {
                return null;
            }
        }

        setLS(prop, value) {
            localStorage.setItem(prop, JSON.stringify(value));
        }

        toObservable(obj, prop) {
            var value = this.getLS(prop) || obj[prop];

            Object.defineProperty(obj, prop, {
                get() {
                    return value;
                },
                set(newValue) {
                    this.setLS(prop, newValue);
                    value = newValue;
                },
            });
        }

        observeProps(obj) {
            for (const [key, _] of Object.entries(obj)) {
                this.toObservable(obj, key);
            }
        }
    }

    const SCROLL_RESET_DELAY = 500;
    const ANIMATION_DELAY = 750;

    class DomManager {
        constructor() {
            GM_addStyle(`
            .filtered-private, .filtered-duration, .filtered-public
            { display: none !important; } `);
        }

        static thumbIsPrivate(t) {
            return t.firstElementChild.classList.contains('private');
        }

        static filterPrivate(container = document.body) {
            const tumbs = $$('.tumbpu', container);
            for (const t of tumbs) {
                toggleClass(t, 'filtered-private',
                            state.filterPrivate && DomManager.thumbIsPrivate(t));
            }
        }

        static filterPublic(container = document.body) {
            const tumbs = $$('.tumbpu', container);
            for (const t of tumbs) {
                toggleClass(t, 'filtered-public',
                            state.filterPublic && !DomManager.thumbIsPrivate(t));
            }
        }

        static filterByDuration(container = document.body) {
            const elements = $$('.duration', container);
            const {
                filterDurationFrom: from,
                filterDurationTo: to,
                filterDuration,
            } = state;
            for (const t of elements) {
                const te = t.parentElement.parentElement;
                let flag = false;
                if (filterDuration) {
                    const ts = timeToSeconds(t.innerText);
                    flag = ts < from || ts > to;
                }
                toggleClass(te, 'filtered-duration', filterDuration && flag);
            }
        }

        static createThumbsContainer() {
            const c = document.createElement('div');
            c.className = 'thumbs-items';
            return c;
        }

        static handleLoadedHTML = (htmlPage, mount, useStateContainer = true) => {
            const thumbs = $$('.tumbpu', htmlPage);

            let container = !useStateContainer
            ? DomManager.createThumbsContainer()
            : (state.container =
               state.container || DomManager.createThumbsContainer());

            for (const thumbElement of thumbs) {
                if (!uniquesQueue.put(thumbElement.getAttribute('href'))) {
                    thumbElement.remove();
                } else {
                    const privateEl = $('.private', thumbElement);
                    const img = $('img', thumbElement);
                    if (privateEl) {
                        img.src = parseCSSUrl(privateEl.style.background);
                        privateEl.style.background = '#000';
                    } else {
                        img.src = img.getAttribute('data-original');
                    }
                    img.classList.add('tracking');
                    container.appendChild(thumbElement);
                }
            }

            if (state.filterPrivate) filterPrivate(container);
            if (state.filterPublic) filterPublic(container);
            if (state.filterDuration) filterByDuration(container);
            mount.before(container);
        };
    }

    class PreviewManager {
        constructor() {
            this.tick = new Tick(ANIMATION_DELAY);
        }

        iteratePreviewImages(src) {
            return src.replace(/(\d)\.jpg$/, (_, num) => circularShift(parseInt(num)) + '.jpg');
        }

        animatePreview = (e) => {
            const { target: el, type } = e;
            if (el.tagName === 'IMG' && el.classList.contains('tracking')) {
                if (type === 'mouseout') {
                    this.tick.stop();
                    if (el.getAttribute('orig')) el.src = el.getAttribute('orig');
                }
                if (type === 'mouseover') {
                    this.tick.start(() => {
                        if (!el.getAttribute('orig')) el.setAttribute('orig', el.src);
                        el.src = this.iteratePreviewImages(el.src);
                    });
                }
            }
        };

        listen(e) {
            e.addEventListener('mouseout', this.animatePreview);
            e.addEventListener('mouseover', this.animatePreview);
        }
    }

    class PaginationPageManager {
        constructor() {
            this.pagination = $('.pagination');
            handleLoadedHTML(document.body, this.pagination);
            previewManager.listen(this.pagination.parentElement);
            this.paginationGenerator = this.createNextPageGenerator();
            this.generatorDone = false;
            this.infiniteScrollTriggered = false;
            this.tick = new Tick(SCROLL_RESET_DELAY);
            this.fixScrollViewPort();
        }

        fixScrollViewPort(){
            this.tick.start(() => {
                if (this.generatorDone) this.tick.stop();
                if (isElementInViewport(this.pagination)) this.generatorConsumer();
            });
        }

        async generatorConsumer() {
            const { value, done } = this.paginationGenerator.next();
            this.generatorDone = done;
            if (!done) {
                const nextPageHTML = await fetchHtml(value);
                const prevScrollPos = document.documentElement.scrollTop;
                handleLoadedHTML(nextPageHTML, this.pagination);
                window.scrollTo(0, prevScrollPos);
            }
        }

        createNextPageGenerator() {
            const pl = $('.pagination-next').previousElementSibling.firstElementChild.textContent;
            const offsetLast = parseInt(pl);

            let {origin, pathname, search } = window.location;
            let offset;
            [pathname, offset='1'] = pathname.split(/(\d+\/)$/);
            offset = parseInt(offset);
            pathname = pathname === '/' ? '/latest-updates/' : pathname;

            function* nextPageGenerator() {
                for (let c = offset + 1; c <= offsetLast; c++) {
                    const url = `${origin}${pathname}${c}/${search}`;
                    console.log(url);
                    yield url;
                }
            }

            return nextPageGenerator();
        }

        resetScroller = () => {
            this.infiniteScrollTriggered = false;
            window.dispatchEvent(new CustomEvent('scroll'));
        };

        infiniteScroll = () => {
            const inViewport = isElementInViewport(this.pagination);
            if (inViewport === this.infiniteScrollTriggered) return;
            this.infiniteScrollTriggered = inViewport;
            if (inViewport) this.generatorConsumer();
        };
    }

    class Router {
        constructor() {
            this.route();
        }

        route() {
            const { pathname } = window.location;
            if ($('.pagination-next') instanceof HTMLElement) {
                this.handlePaginationPage();
            } else if (/\/members\/\d+\/$/.test(pathname)) {
                this.handleMemberPage();
            } else if (/\/tag\//.test(pathname) || /\/?q=.*/.test(pathname)) {
                this.handlePageWithVideosButNoPagination();
            }
        }

        handlePageWithVideosButNoPagination() {
            const vid = $('.tumbpu');
            if (!vid) return;
            handleLoadedHTML(document.body, vid.parentElement);
            previewManager.listen(vid.parentElement);
        }

        handlePaginationPage() {
            this.paginationManager = new PaginationPageManager();
        }

        handleMemberPage() {
            const privates = $('#list_videos_private_videos_items');
            if (privates) {
                const mistakes = findElementsBetweenIds(
                    'list_videos_private_videos_items',
                    'list_videos_favourite_videos'
                );
                mistakes.forEach((m) => privates.appendChild(m));
                handleLoadedHTML(privates, privates, false);
            }

            const favorites = $('#list_videos_favourite_videos');
            if (favorites) {
                const mountTo = favorites.firstElementChild.nextElementSibling;
                handleLoadedHTML(favorites, mountTo, false);
            }

            if (privates || favorites) {
                previewManager.listen((privates || favorites).parentElement);
            }
        }
    }

    class UI {
        templateHTML = `
<div id="tapermonkey-app">
  <div class="subbox">
    <input type="checkbox" id="filterPrivate" name="filterPrivate" ${
      state.filterPrivate ? 'checked' : ''
    }/>
    <label for="filterPrivate">filter private</label>
    <input type="checkbox" id="filterPublic" name="filterPublic" ${
      state.filterPublic ? 'checked' : ''
    }/>
    <label for="filterPublic">filter public</label>
  </div>
  <div class="subbox">
    <input type="checkbox" id="filterl" name="filterl" ${
      state.filterDuration ? 'checked' : ''
    } />
    <label for="filterl">filter duration seconds</label>

    <input type="number" placeholder="min sec" step="10"
      min="0" max="100000" id="minL" value=${state.filterDurationFrom} />
    <input type="number" placeholder="max sec" step="10"
      min="0" max="100000" id="maxL" value=${state.filterDurationTo} />
  </div>
</div>`;

        templateCSS = `
    #tapermonkey-app {
      background: #151515;
      color: #fff;
      padding: 10px;
      position: fixed;
      z-index: 9999;
      bottom: 10px;
      right: 10px;
      border-radius: 15px;
      width: max-content;
      box-shadow: 20px 20px 60px #000000, -20px -20px 60px #000000;
    }
    #tapermonkey-app .subbox {
      background: #2c2c2c;
      border-radius: 5px;
      padding: 4px;
      margin: 6px;
    }
    #tapermonkey-app .subbox input[type=number] {
      padding-left: 10px;
      width: 5rem;
      background: #26282b;
    }
    #tapermonkey-app .subbox input[type=checkbox] {
      margin-left: 5px;
    }
    #tapermonkey-app .subbox label {
      user-select: none;
    }
    #tapermonkey-app .subbox input,
    #tapermonkey-app .subbox label {
      padding-left: 8px;
      float: none;
      width: auto;
      font-family: monospace;
      font-size: 0.8rem;
    }`;

        constructor() {
            document.body.appendChild(parseDom(this.templateHTML));
            this.tapermonkeyAppTemplate = document.querySelector('#tapermonkey-app');
            GM_addStyle(this.templateCSS);
            this.control();
        }

        control() {
            this.tapermonkeyAppTemplate.addEventListener('click', (e) => {
                if (e.target.id === 'filterPublic') {
                    state.filterPublic = e.target.checked;
                    filterPublic();
                }
                if (e.target.id === 'filterPrivate') {
                    state.filterPrivate = e.target.checked;
                    filterPrivate();
                }
                if (e.target.id === 'filterl') {
                    state.filterDuration = e.target.checked;
                    filterByDuration();
                }
                if (e.target.id === 'minL') {
                    state.filterDurationFrom =
                        parseInt(e.target.value) || state.filterDurationFrom;
                    filterByDuration();
                }
                if (e.target.id === 'maxL') {
                    state.filterDurationTo =
                        parseInt(e.target.value) || state.filterDurationTo;
                    filterByDuration();
                }
            });
        }
    }

    const state = new ReactiveLocalStorage({
        filterDurationFrom: 600,
        filterDurationTo: 100000,
        filterDuration: false,
        filterPrivate: false,
        filterPublic: false,
        infiniteScrollTriggered: false,
    });
    const uniquesQueue = new QueueDuplicatesFilter();
    const domManager = new DomManager();
    const { filterPrivate, filterPublic, filterByDuration, handleLoadedHTML } = DomManager;
    const previewManager = new PreviewManager();
    const router = new Router();
    const ui = new UI();
})();