thisvid infinite scroll and filter

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

目前為 2024-02-03 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         thisvid infinite scroll and filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  infinite scroll, filter: public, private, duration, positive/negative tags
// @author       smartacephale
// @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/*
// @match        https://thisvid.com/videos/*
// @icon         https://i.imgur.com/LAhknzo.jpeg
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

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

(function () {
    // biome-ignore lint/suspicious/noRedundantUseStrict: <explanation>
    'use strict';
    console.clear();
    console.log(logo);

    // biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
    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 } = window.location;
            if ($('.pagination-next')) {
                this.handlePaginationPage();
            } else if (/\/members\/\d+\/$/.test(pathname)) {
                this.handleMemberPage();
            } else if (/\/tag\//.test(pathname) || /\/?q=.*/.test(pathname)) {
                this.handlePageWithVideosButNoPagination();
            } else if (/\/videos\//.test(pathname)) {
                //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);
})();