thisvid infinite scroll and filter

infinite scroll, filter: public, private, duration, positive/negative tags

Verzia zo dňa 19.02.2024. Pozri najnovšiu verziu.

// ==UserScript==
// @name         thisvid infinite scroll and filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  infinite scroll, filter: public, private, duration, positive/negative tags
// @author       smartacephale
// @match        https://thisvid.com/*
// @exclude      https://thisvid.com/playlists/*
// @exclude      https://thisvid.com/community/*
// @exclude      https://thisvid.com/albums/*
// @exclude      https://thisvid.com/my*
// @icon         https://i.imgur.com/LAhknzo.jpeg
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

const logo = `
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░║▓▓▓▀░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░▒▓▓▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░▒▒▒▒▒░░░░░░░░░░╫█▓▓▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░▒▒▒▒▒░▒░░░░░░░░░░▒▒▒▒▒▒@@▒▒▒▒╖░▓▓▓▓▓▒░░░░░░░░░░░░░▒▒▒▒▒░░░░░░░░░░░░░
░░░░░░░░░▒▒▒▒▒▒░▒░░░░░░░░░░░░░░░▒▒▒▒╣╣╢╢╣╣╣▓▓▓▓▓▓▓▓▓▓▓╣╣▒▒▒▒▒░░░░░░░░▒▒░░░░░░░░░
░░░░░░░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░▒▒▒▒╢╢╫▓▓▓▓▓▓▓▓▓▓▓▓▓╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒╢▓▓▓▓▓▓▓▓▓▓▓▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░▒░░░▒▒▒▒╫▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒╫▓▓▓▓▓▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒╫▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒╢▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒╢╣▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒╣╣╣▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒╣╣▒▒▒╢▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒░▒▒▒▒▒▒▒╢╢╣▒▒╢╣▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒╢╣╣▒▒╢▓▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒╢╫╣╣▒▒╢▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒░▒▒╣╢╣▒▒▒▒▓╣▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░▒▒▒▒▒▒╫╣▒▒▒▒▒╫╣▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░▒▒╢╢▒▒╢╣╣╢╫╣╣▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒╢╫╣╣▒╢╣▒▒▒╣▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒╢▒▒▒╢▓▓╣▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓╣▓╣▒▒▒▒░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░▒░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒░▒▒▒╢▓▓▓╬▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒╢▓╣▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒╣╣▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒╣╣╣▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒╣╢╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒╢▒╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒╢▓▓╣╫▓╣▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒╫▓▓▓▓▓▓╣▒▒░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░
▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░▒▒▒▒╫▓▓▓▓▓▓▓▓▓╣╣▒▒░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░
▒▒▒▒▒▒▒▒╣▒╣▒▒▒░▒░░░░░░░░░░░░░░▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓╣╫╣▒▒░░░░░░░░░░░░░░▒▒░░░░░░░░░░
▒▒▒▒▒▒▒▒╢╣╢╢▓▓╣▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓╣▒▒▒▒▒▒▒░░░▒▒▒▒▒░░░░░░░░░░░
░░░░░░░░░░░░▒▒▒╨╨╢▒▒▒▒▒▒▒▒▒▒╨╨╜╜▒▒▒▒▒▒╜▒▒╨╨╨╨╨╨╨╨╨╨╨▀▒╫▒▒╜╣╫╝,░▒▒░]▒▒▒▒╨▒▒░╜Ñ▒╓─`.trim();

(function () {
    'use strict';
    console.clear();
    console.log(logo);

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

        static parseDOM = (html) => {
            const parsed = new DOMParser().parseFromString(html, 'text/html');
            return parsed.body.children.length > 1 ? parsed.body : parsed.body.firstElementChild;
        }

        static fetchHtml = (url) => fetch(url).then((r) => r.text()).then((h) => parseDOM(h));

        static parseCSSUrl = (s) => s.replace(/url\("|\"\).*/g, '');

        static timeToSeconds = (t) =>
        (t.match(/\d+/gm) || ['0'])
        .reverse()
        .map((s, i) => parseInt(s) * 60 ** i)
        .reduce((a, b) => a + b) || 0;

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

        static parseIntegerOr = (n, or) =>
        Number.isInteger(parseInt(n)) ? parseInt(n) : or;
    }

    const {
        $,
        $$,
        parseDOM,
        fetchHtml,
        parseCSSUrl,
        circularShift,
        timeToSeconds,
        parseIntegerOr,
    } = Utils;

    class IntersectionObserver_ {
        constructor(callback) {
            this.callback = callback;
            this.observer = this.createObserver();
        }

        createObserver() {
            return new IntersectionObserver((entries, observer) => {
                for (const entry of entries) {
                    if (entry.isIntersecting) {
                        this.callback(entry.target, observer);
                    }
                }
            });
        }

        observe(element) {
            this.observer.observe(element);
        }
    }

    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 ReactiveLocalStorage {
        constructor(data) {
            if (data) {
                Object.assign(this, data);
                this.observeProps(this);
            }
        }

        getLS(prop) {
            return JSON.parse(localStorage.getItem(prop));
        }

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

        toObservable(obj, prop) {
            const lsvalue = this.getLS(prop);
            let value = lsvalue !== null ? lsvalue : 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);
            }
        }
    }

    class DomManager {
        constructor() {
            this.data = new Map();
            this.container = this.createThumbsContainer();
            this.observables = [];
            this.lazyImageObserver = new IntersectionObserver_((target, observer) => {
                if (!this.isFiltered(target)) {
                    observer.unobserve(target);
                    this.delazify(target);
                }
            });
        }

        setObservation() {
            for (const observable of this.observables) {
                this.lazyImageObserver.observe(observable);
            }
            this.observables = [];
        }

        delazify(el) {
            const img = el.firstElementChild.firstElementChild;
            img.src = img.getAttribute('lazy-loading');
        }

        isFiltered(el) {
            return el.className.includes('filtered');
        }

        filterPositiveTags = () => {
            for (const [k, v] of this.data.entries()) {
                const containTagsNot = state.filterPositiveTags.some(tag => !k.includes(tag));
                v.element.classList.toggle('filtered-tag-pos', state.filterPositive && containTagsNot);
            };
        }

        filterNegativeTags = () => {
            for (const [k, v] of this.data.entries()) {
                const containTags = state.filterNegativeTags.some(tag => k.includes(tag));
                v.element.classList.toggle('filtered-tag-neg', state.filterNegative && containTags);
            };
        }

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

        filterPrivate = (filterPrivate = state.filterPrivate) => {
            for (const v of this.data.values()) {
                v.element.classList.toggle('filtered-private',
                                           filterPrivate && this.thumbIsPrivate(v.element))
            };
        }

        filterPublic = (filterPublic = state.filterPublic) => {
            for (const v of this.data.values()) {
                v.element.classList.toggle('filtered-public',
                                           filterPublic && !this.thumbIsPrivate(v.element));
            };
        }

        filterByDuration = () => {
            const { filterDurationFrom: from, filterDurationTo: to, filterDuration } = state;
            for (const v of this.data.values()) {
                v.element.classList.toggle('filtered-duration',
                                           filterDuration && (v.duration < from || v.duration > to));
            };
        }

        runFilters() {
            if (state.filterPrivate) this.filterPrivate();
            if (state.filterPublic) this.filterPublic();
            if (state.filterDuration) this.filterByDuration();
            if (state.filterPositive) this.filterPositiveTags();
            if (state.filterNegative) this.filterNegativeTags();
        }

        createThumbsContainer() {
            return parseDOM('<div class="thumbs-items"></div>');
        }

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

            const container = !useGlobalContainer ? this.createThumbsContainer() : this.container;

            for (const thumbElement of thumbs) {
                const url = thumbElement.getAttribute('href');
                if (!url || this.data.has(url)) {
                    thumbElement.remove();
                } else {
                    this.data.set(url, {
                        element: thumbElement,
                        duration: timeToSeconds($('.thumb > .duration', thumbElement).textContent),
                    });
                    const img = $('img', thumbElement);
                    const thumb2 = $('.private', thumbElement);
                    let imgSrc;
                    if (thumb2) {
                        imgSrc = parseCSSUrl(thumb2.style.background);
                        thumb2.style.removeProperty('background');
                        thumb2.removeAttribute('style');
                    } else {
                        imgSrc = img.getAttribute('data-original');
                        img.removeAttribute('data-original');
                    }
                    img.setAttribute('lazy-loading', imgSrc);
                    img.src = '';
                    img.classList.add('tracking');
                    img.removeAttribute('data-cnt');

                    container.appendChild(thumbElement);
                    this.observables.push(thumbElement);
                }
            }

            this.runFilters(container);
            this.setObservation();
            mount.before(container);
        };
    }

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

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

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

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

    class PaginationPageManager {
        constructor() {
            this.pagination = $('.pagination');
            this.pagination.style.opacity = 0;
            unsafeWindow.$('img[alt!="Private"]').off('mouseover');
            unsafeWindow.$('img[alt!="Private"]').off('mouseout');
            handleLoadedHTML(document.body, this.pagination);
            previewManager.listen(this.pagination.parentElement);

            this.offsetLast = this.getOffsetLast();
            this.paginationGenerator = this.createNextPageGenerator();
            this.generatorDone = false;

            this.ui = new UI(true);
            this.setPagIndex = (offset) =>
            this.ui.setPagIndex(offset, this.offsetLast);
            this.setPagIndex(this.getCurrentOffset());

            this.paginationObserver = new IntersectionObserver_((target, observer) => {
                this.generatorConsumer();
                if (!this.generatorDone) {
                    observer.unobserve(target);
                    setTimeout(() => {
                        observer.observe(target);
                    }, SCROLL_RESET_DELAY);
                }
            });

            this.paginationObserver.observe(this.pagination);
        }

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

        getCurrentOffset() {
            return parseInt(window.location.pathname.split(/(\d+\/)$/)[1] || '1');
        }

        getOffsetLast() {
            return parseInt(
                $('.pagination-next').previousElementSibling.firstElementChild
                .textContent,
            );
        }

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

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

            return nextPageGenerator();
        }
    }

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

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

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

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

        handleMemberPage() {
            const privates = $('#list_videos_private_videos_items');
            if (privates) {
                const mistakes = document.querySelectorAll('#list_videos_private_videos_items ~ div');
                for (const m of mistakes) {
                    if (m.id === 'list_videos_favourite_videos') break;
                    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);
                const ui = new UI(false);
            }
        }
    }

    class UI {
        templateHTML = (haspag) => `
<div id="tapermonkey-app">
<div class="subbox">
    <input type="checkbox" id="filterNegative" name="filterNegative" ${state.filterNegative ? 'checked' : ''}/>
    <label for="filterNegative">negative filter</label>
    <textarea id="filterNegativeText" placeholder="tag1, tag2,..">${state.filterNegativeTags.join(',')}</textarea>
</div>
<div class="subbox">
    <input type="checkbox" id="filterPositive" name="filterPositive" ${state.filterPositive ? 'checked' : ''}/>
    <label for="filterPositive">positive filter</label>
    <textarea id="filterPositiveText" placeholder="tag1, tag2,..">${state.filterPositiveTags.join(',')}</textarea>
</div>
<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>
  ${haspag ? '<span id="pagIndex">0/0</span>' : ''}
</div>
<div class="subbox">
  <input type="checkbox" id="filterl" name="filterl" ${state.filterDuration ? 'checked' : ''}/>
  <label for="filterl">filter duration seconds</label>

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

        constructor(haspag = true) {
            document.body.appendChild(parseDOM(this.templateHTML(haspag)));
            this.tapermonkeyAppTemplate = document.querySelector('#tapermonkey-app');
            this.control();
        }

        setPagIndex(index, total) {
            $('#pagIndex').innerText = `${index}/${total}`;
        }

        stringToTags(s) {
            return s.split(",").map(s => s.trim()).filter(s => s.length > 0);
        }

        control() {
            this.tapermonkeyAppTemplate.addEventListener('input', (e) => {
                const { id, value } = e.target;
                if (id === 'filterNegativeText') {
                    state.filterNegativeTags = this.stringToTags(value);
                    filterNegativeTags()
                }
                if (id === 'filterPositiveText') {
                    state.filterPositiveTags = this.stringToTags(value);
                    filterPositiveTags();
                }
            });

            this.tapermonkeyAppTemplate.addEventListener('click', (e) => {
                const { id, checked, value } = e.target;
                if (id === 'filterPublic') {
                    state.filterPublic = checked;
                    filterPublic();
                }
                if (id === 'filterPrivate') {
                    state.filterPrivate = checked;
                    filterPrivate();
                }
                if (id === 'filterNegative') {
                    state.filterNegative = checked;
                    filterNegativeTags()
                }
                if (id === 'filterPositive') {
                    state.filterPositive = checked;
                    filterPositiveTags();
                }
                if (id === 'filterl') {
                    state.filterDuration = checked;
                    filterByDuration();
                }
                if (id === 'minL') {
                    state.filterDurationFrom = parseIntegerOr(value, state.filterDurationFrom);
                    filterByDuration();
                }
                if (id === 'maxL') {
                    state.filterDurationTo = parseIntegerOr(value, state.filterDurationTo);
                    filterByDuration();
                }
            });
        }
    }

    const SCROLL_RESET_DELAY = 500;
    const ANIMATION_DELAY = 750;

    const state = new ReactiveLocalStorage({
        filterDurationFrom: 0,
        filterDurationTo: 600,
        filterDuration: false,
        filterPrivate: false,
        filterPublic: false,
        infiniteScrollTriggered: false,
        filterNegativeTags: [],
        filterNegative: false,
        filterPositiveTags: [],
        filterPositive: false
    });

    const { filterPrivate, filterPublic, filterByDuration, filterPositiveTags, filterNegativeTags, handleLoadedHTML } = new DomManager();
    const previewManager = new PreviewManager();
    const router = new Router();

    const tampermonkeyCSS = `
#tapermonkey-app {
  background: #151515;
  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;
  user-select: none;
  display: flex;
}
#tapermonkey-app .subbox input[type=number] {
  width: 5rem;
  background: #26282b;
}
#tapermonkey-app .subbox input[type=checkbox] { margin-left: 5px; }
#tapermonkey-app .subbox label { margin: 0 10px 0 0; }
#pagIndex { text-align: end; margin-right: 5px; flex: 1;}
#tapermonkey-app textarea { flex: 1; height: 2rem; padding: 6px; }
#tapermonkey-app .subbox * {
  padding-left: 8px;
  float: none;
  width: auto;
  font-family: monospace;
  font-size: 0.8rem;
  align-self: center;
  color: #969696;
}

.tracking { content-visibility: auto; }
.filtered-private, .filtered-duration, .filtered-public, .filtered-tag-pos, .filtered-tag-neg
        { display: none !important; } `;

    GM_addStyle(tampermonkeyCSS);
})();