thisvid infinite scroll and filter

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

От 30.01.2024. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         thisvid infinite scroll and filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2.0.1
// @description  [s]try to take over the world![/s] infinite scroll, filter: public, private, duration
// @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         data:image/jpeg;base64,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
// @grant        GM_addStyle
// @grant        GM_addElement
// @run-at       document-end
// ==/UserScript==
(function () {
  // biome-ignore lint/suspicious/noRedundantUseStrict: <explanation>
  'use strict';

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

  // 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 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) => {
      const 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) || ['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 isElementInViewport = (el) => {
      const 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;
    };

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

  const {
    $,
    $$,
    findElementsBetweenIds,
    fetchHtml,
    parseHTML,
    parseCSSUrl,
    circularShift,
    isElementInViewport,
    timeToSeconds,
    getRandomRgb,
    toggleClass,
    parseDom,
    parseIntegerOr,
  } = 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 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);
      }
    }
  }

  const SCROLL_RESET_DELAY = 500;
  const ANIMATION_DELAY = 750;

  // biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
  class DomManager {
    static thumbIsPrivate(t) {
      return t.firstElementChild.classList.contains('private');
    }

    static filterPrivate(container = document.body) {
      // biome-ignore lint/complexity/noForEach: <explanation>
      data.forEach((v) => toggleClass(v.element, 'filtered-private',
        state.filterPrivate && thumbIsPrivate(v.element)));
    }

    static filterPublic(container = document.body) {
      // biome-ignore lint/complexity/noForEach: <explanation>
      data.forEach((v) => toggleClass(v.element, 'filtered-public',
        state.filterPublic && !thumbIsPrivate(v.element)));
    }

    static filterByDuration(container = document.body) {
      const {
        filterDurationFrom: from,
        filterDurationTo: to,
        filterDuration,
      } = state;
      // biome-ignore lint/complexity/noForEach: <explanation>
      data.forEach((v) => toggleClass(v.element, 'filtered-duration',
        filterDuration && (v.duration < from || v.duration > to)));
    }

    static runFilters(container) {
      if (state.filterPrivate) filterPrivate(container);
      if (state.filterPublic) filterPublic(container);
      if (state.filterDuration) filterByDuration(container);
    }

    static createThumbsContainer() {
      return GM_addElement('div', { class: 'thumbs-items' });
    }

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

      const container = !useStateContainer
        ? createThumbsContainer()
        : containerGlobal;

      for (const thumbElement of thumbs) {
        const url = thumbElement.getAttribute('href');
        if (!url || data.has(url)) {
          thumbElement.remove();
        } else {
          data.set(url, {
            element: thumbElement,
            duration: timeToSeconds($('.thumb > .duration', thumbElement).textContent)
          });
          const img = $('img', thumbElement);
          const privateEl = $('.private', 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);
        }
      }

      DomManager.runFilters(container);
      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 } = 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') {
          if (!el.getAttribute('orig')) el.setAttribute('orig', el.src);
          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;
      handleLoadedHTML(document.body, this.pagination);
      previewManager.listen(this.pagination.parentElement);

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

      this.resetScroller();
      this.tick = new Tick(SCROLL_RESET_DELAY);
      this.fixScrollViewPort();

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

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

    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();
    }

    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')) {
        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 = findElementsBetweenIds(
          'list_videos_private_videos_items',
          'list_videos_favourite_videos',
        );
        for (const m of mistakes) privates.appendChild(m);
        handleLoadedHTML(privates, privates, false);
        const ui = new UI(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 = (haspag) => `
<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>
    ${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="100000" id="minL" name="from" value=${state.filterDurationFrom} />
    <label for="to">to</label>
    <input type="number" placeholder="max sec" step="10"
      min="0" max="100000" 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}`;
    }

    control() {
      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 === '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 state = new ReactiveLocalStorage({
    filterDurationFrom: 0,
    filterDurationTo: 100000,
    filterDuration: false,
    filterPrivate: false,
    filterPublic: false,
    infiniteScrollTriggered: false,
  });

  const data = new Map();
  const { filterPrivate, filterPublic, filterByDuration, handleLoadedHTML, createThumbsContainer, thumbIsPrivate } = DomManager;
  const containerGlobal = createThumbsContainer();
  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] {
      padding-left: 10px;
      width: 5rem;
      background: #26282b;
    }
    #tapermonkey-app .subbox input[type=checkbox] { margin-left: 5px; }
    #tapermonkey-app .subbox label { margin: 0 10px 0 0; }
    #pagIndex { color: #969696; text-align: end; margin-right: 5px; flex: 1;}
    #tapermonkey-app .subbox * {
      padding-left: 8px;
      float: none;
      width: auto;
      font-family: monospace;
      font-size: 0.8rem;
      align-self: center;
    }

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

  GM_addStyle(tampermonkeyCSS);
})();