thisvid infinite scroll and filter

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

Per 29-02-2024. Zie de nieuwste versie.

// ==UserScript==
// @name         thisvid infinite scroll and filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2.6.1
// @description  infinite scroll, filter: public, private, duration, positive/negative tags
// @author       smartacephale
// @match        https://*.thisvid.com/*
// @exclude      https://*.thisvid.com/playlist*
// @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 ICON = `
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░║▓▓▓▀░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░▒▓▓▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░▒▒▒▒▒░░░░░░░░░░╫█▓▓▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░▒▒▒▒▒░▒░░░░░░░░░░▒▒▒▒▒▒@@▒▒▒▒╖░▓▓▓▓▓▒░░░░░░░░░░░░░▒▒▒▒▒░░░░░░░░░░░░░
░░░░░░░░░▒▒▒▒▒▒░▒░░░░░░░░░░░░░░░▒▒▒▒╣╣╢╢╣╣╣▓▓▓▓▓▓▓▓▓▓▓╣╣▒▒▒▒▒░░░░░░░░▒▒░░░░░░░░░
░░░░░░░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░▒▒▒▒╢╢╫▓▓▓▓▓▓▓▓▓▓▓▓▓╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒╢▓▓▓▓▓▓▓▓▓▓▓▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░▒░░░▒▒▒▒╫▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒╫▓▓▓▓▓▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒╫▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒╢▓▓▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒╢╣▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒╣╣╣▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒╣╣▒▒▒╢▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒░▒▒▒▒▒▒▒╢╢╣▒▒╢╣▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒╢╣╣▒▒╢▓▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒╢╫╣╣▒▒╢▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒░▒▒╣╢╣▒▒▒▒▓╣▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░▒▒▒▒▒▒╫╣▒▒▒▒▒╫╣▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░▒▒╢╢▒▒╢╣╣╢╫╣╣▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒╢╫╣╣▒╢╣▒▒▒╣▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒╢▒▒▒╢▓▓╣▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓╣▓╣▒▒▒▒░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░▒░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒░▒▒▒╢▓▓▓╬▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒╢▓╣▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒╣╣▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒╣╣╣▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒╣╢╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒╢▒╣▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒╢▓▓╣╫▓╣▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒╫▓▓▓▓▓▓╣▒▒░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░
▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░▒▒▒▒╫▓▓▓▓▓▓▓▓▓╣╣▒▒░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░
▒▒▒▒▒▒▒▒╣▒╣▒▒▒░▒░░░░░░░░░░░░░░▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓╣╫╣▒▒░░░░░░░░░░░░░░▒▒░░░░░░░░░░
▒▒▒▒▒▒▒▒╢╣╢╢▓▓╣▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓▓▓▓╣▒▒▒▒▒▒▒░░░▒▒▒▒▒░░░░░░░░░░░
░░░░░░░░░░░░▒▒▒╨╨╢▒▒▒▒▒▒▒▒▒▒╨╨╜╜▒▒▒▒▒▒╜▒▒╨╨╨╨╨╨╨╨╨╨╨▀▒╫▒▒╜╣╫╝,░▒▒░]▒▒▒▒╨▒▒░╜Ñ▒╓─`.trim();

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

  function $(x, e = document.body) { return e.querySelector(x); }
  function $$(x, e = document.body) { return e.querySelectorAll(x); }

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

  function fetchHtml(url) { return fetch(url).then((r) => r.text()).then((h) => parseDOM(h)); }

  function parseCSSUrl(s) { return s.replace(/url\("|\"\).*/g, ''); }

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

  function circularShift(n, c = 6, s = 1) { return (n + s) % c || c; }

  function parseIntegerOr(n, or) {
    return Number.isInteger(parseInt(n)) ? parseInt(n) : or;
  }

  class Observer {
    constructor(callback) {
      this.callback = callback;
      this.observer = this.create();
    }

    create() {
      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 Observer((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();
          continue;
        }
        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')) return;
      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 Observer((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" ${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" ${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" ${state.filterPrivate ? 'checked' : ''}/>
  <label for="filterPrivate">filter private</label>
  <input type="checkbox" id="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" ${state.filterDuration ? 'checked' : ''}/>
  <label for="filterl">filter duration seconds</label>
  <label>from</label>
  <input type="number" step="10" min="0" max="72000" id="minL" value=${state.filterDurationFrom} />
  <label>to</label>
  <input type="number" step="10" min="0" max="72000" id="maxL" 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: #16181b;
  padding: 6px;
  position: fixed;
  z-index: 9999;
  bottom: 10px;
  right: 10px;
  width: max-content;
}
#tapermonkey-app .subbox {
  background: #000;
  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; background: #26282b; }
#tapermonkey-app .subbox * {
  padding-left: 8px;
  width: auto;
  font-family: monospace;
  font-size: 0.85rem;
  align-self: center;
}
.filtered-private, .filtered-duration, .filtered-public, .filtered-tag-pos, .filtered-tag-neg { display: none !important; } `;

  GM_addStyle(tampermonkeyCSS);
})();