thisvid.com infinite scroll and filter

features: infinite scroll, lazy loading, preview for private videos, filter: duration, public/private, include/exclude terms, mass friend request

2024-04-10 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         thisvid.com infinite scroll and filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      3.6.1
// @description  features: infinite scroll, lazy loading, preview for private videos, filter: duration, public/private, include/exclude terms, mass friend request
// @author       smartacephale
// @match        https://*.thisvid.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=thisvid.com
// @grant        GM_addStyle
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/rxjs.umd.min.js
// @require      https://unpkg.com/[email protected]/dist/vue.global.prod.js
// ==/UserScript==

const SponsaaLogo = `
  Kono bangumi ha(wa) goran no suponsaa no teikyou de okurishimasu⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡟⣟⢻⢛⢟⠿⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣾⣾⣵⣧⣷⢽⢮⢧⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣿⣯⣭⣧⣯⣮⣧⣯⣧⣯⡮⣵⣱⢕⣕⢕⣕⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⡫⡻⣝⢯⡻⣝⡟⣟⢽⡫⡟⣏⢏⡏⡝⡭⡹⡩⣻⣿⣿⣿⣿⠟⠟⢟⡟⠟⠻⠛⠟⠻⠻⣿⣿⣿⡟⠟⠻⠛⠟⠻⠻⣿⣿
  ⣿⣿⣿⣿⡿⣻⣿⣿⣿⡿⣿⣿⡿⣿⣿⢿⢿⡻⢾⠽⡺⡞⣗⠷⣿⣿⣿⡏⠀⠀⠀⣣⣤⡄⠀⠠⣄⡆⠫⠋⠻⢕⣤⡄⠀ ⢀⣤⣔⣿⣿
  ⣿⣿⣿⣿⣷⣷⣷⣾⣶⣯⣶⣶⣷⣷⣾⣷⣳⣵⣧⣳⡵⣕⣮⣞⣾⣿⡟⠄⢀⣦⠀⢘⣽⡇⠀⠨⣿⡌⠀⠣⣠⠹⠿⡭⠀ ⠐⣿⣿⣿⣿
  ⣿⣿⣿⣿⣕⣵⣱⣫⣳⡯⣯⣫⣯⣞⣮⣎⣮⣪⣢⣣⣝⣜⡜⣜⣾⣿⠃⠀⠀⠑⠀⠀⢺⡇⠀ ⢘⣾⠀⢄⢄⠘⠀⢘⢎⠀⢈⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣙⣛⣛⢻⢛⢟⢟⣛⢻⢹⣙⢳⢹⢚⢕⣓⡓⡏⣗⣿⣓⣀⣀⣿⣿⣮⢀⣀⣇⣀⣐⣿⣔⣀⢁⢀⣀⣀⣅⣀⡠⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣾⡞⣞⢷⡻⡯⡷⣗⢯⢷⢞⢷⢻⢞⢷⡳⣻⣺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣿⣷⣵⡵⣼⢼⢼⡴⣵⢵⡵⣵⢵⡵⣵⣪⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⣧⣫⣪⡪⡣⣫⣪⣣⣯⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿`.trim();

//====================================================================================================

unsafeWindow.Vue = Vue;
const { ref, watch, reactive, createApp } = Vue;

//====================================================================================================

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 fetchText(url) { return fetch(url).then((r) => r.text()); }

function range(size, startAt = 1) {
  return [...Array(size).keys()].map(i => i + startAt);
}

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

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

function listenEvents(dom, events, callback) {
  for (const e of events) {
    dom.addEventListener(e, callback, true);
  }
}

//====================================================================================================

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

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

  throttle(target, throttleTime) {
    this.observer.unobserve(target);
    setTimeout(() => this.observer.observe(target), throttleTime);
  }

  static observeWhile(target, callback, throttleTime) {
    const observer_ = new Observer(async (target) => {
      const condition = await callback();
      if (condition) observer_.throttle(target, throttleTime);
    });

    observer_.observe(target);

    return observer_;
  }
}

//====================================================================================================

class LazyImgLoader {
  attributeName = 'lazy-loading';

  constructor(callback) {
    this.lazyImgObserver = new Observer((target) => {
      callback(target, this.delazify);
    });
  }

  lazify(parent, img, src) {
    img.setAttribute(this.attributeName, src);
    img.src = '';
    this.lazyImgObserver.observe(parent);
  }

  delazify = (target) => {
    this.lazyImgObserver.observer.unobserve(target);
    const img = $('img', target);
    img.src = img.getAttribute(this.attributeName);
    // img.removeAttribute(this.attributeName)
  }
}

//====================================================================================================

class PersistentState {
  key = "state__";

  constructor(state) {
    this.state = reactive(state);
    this.trySetFromLocalStorage();
    this.watchPersistence();
  }

  watchPersistence() {
    watch(this.state, (value) => {
      this.saveToLocalStorage(this.key, value);
    });
  }

  saveToLocalStorage(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  trySetFromLocalStorage() {
    const localStorageValue = localStorage.getItem(this.key);
    if (localStorageValue !== null) {
      const prevState = JSON.parse(localStorageValue);
      for (const prop of Object.keys(prevState)) {
        this.state[prop] = prevState[prop];
      }
    }
  }
}

//====================================================================================================

function initFriendship() {
  if ($('.my-avatar') ||
    !$('.js-del-cookie') ||
    !/members\/\d+\/$/.test(window.location.href)) return;

  createFriendButton();

  function getUsers(el) {
    return Array.from(
      el.querySelector('#list_members_friends_items')
        .querySelectorAll('.tumbpu'))
      .map(e => e.href.match(/\d+/)[0]);
  }

  function friend(id, i = 0) {
    return fetchText(FRIEND_REQUEST_URL(id)).then((text) =>
      console.log(`friend request #${i} with https://thisvid.com/members/662717/${id}/`, text));
  }

  const FRIEND_REQUEST_URL = (id) => `https://thisvid.com/members/${id}/?action=add_to_friends_complete&function=get_block&block_id=member_profile_view_view_profile&format=json&mode=async&message=`;
  const USERS_PER_PAGE = 24;

  async function friendMemberFriends() {
    const memberId = window.location.pathname.match(/\d+/)[0];
    const friendsEl = $('#list_members_friends').firstElementChild.innerText.match(/\d+/g);
    const friendsCount = parseInt(friendsEl[friendsEl.length - 1]);
    let friends;
    if (friendsCount > 12) {
      const offset = Math.ceil(friendsCount / USERS_PER_PAGE);
      const pages = range(offset).map(o => `https://thisvid.com/members/${memberId}/friends/${o}/`);
      const pagesFetched = pages.map(p => fetchHtml(p).then(h => getUsers(h)));
      friends = (await Promise.all(pagesFetched)).flat();
    } else {
      friends = getUsers(document.body);
    }
    await Promise.all(friends.map((fid, i) => friend(fid, i)));
    await friend(memberId);
  }

  function createFriendButton() {
    const button = parseDOM('<button style="background: radial-gradient(red, blueviolet);">friend everyone</button>');
    const container = $('.buttons');
    container.appendChild(button);
    GM_addStyle('.buttons {display: flex; flex-wrap: wrap} .buttons * {align-self: center; padding: 3px; margin: 1px;}');
    button.addEventListener('click', () => {
      button.style.background = 'radial-gradient(#ff6114, #5babc4)';
      button.innerText = 'processing requests';
      friendMemberFriends().then(() => {
        button.style.background = 'radial-gradient(blue, lightgreen)';
        button.innerText = 'friend requests sent';
      });
    }, { once: true });
  }
}

//====================================================================================================

const SCROLL_RESET_DELAY = 500;
const ANIMATION_DELAY = 750;

//====================================================================================================

class THISVID_RULES {
  constructor() {
    this.PAGINATION = $('.pagination');
    this.PAGE_HAS_VIDEO = $('.tumbpu');
    this.PAGINATION_LAST = this.PAGINATION ?
      parseInt($('.pagination-next').previousElementSibling.firstElementChild.textContent) : 1;
  }

  URL_DATA() {
    let { origin, pathname, search } = window.location;

    const offset = parseInt(pathname.split(/(\d+\/)$/)[1] || '1');

    let pathname_ = pathname.split(/(\d+\/)$/)[0];
    if (pathname === '/') pathname_ = '/latest-updates/';

    const iteratable_url = (n) => `${origin}${pathname_}${n}/${search}`;

    return {
      offset,
      iteratable_url
    }
  }

  PREVIEW_IMG_DATA(thumbElement) {
    const img = $('img', thumbElement);
    const privateThumb = $('.private', thumbElement);

    let imgSrc;

    if (privateThumb) {
      imgSrc = parseCSSUrl(privateThumb.style.background);
      privateThumb.removeAttribute('style');
    } else {
      imgSrc = img.getAttribute('data-original');
      img.removeAttribute('data-original');
    }

    img.removeAttribute('data-cnt');

    return { img, imgSrc };
  }

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

  GET_VIDEO_DURATION(thumbElement) {
    return timeToSeconds($('.thumb > .duration', thumbElement).textContent);
  }

  IS_PRIVATE(thumbElement) {
    return thumbElement.firstElementChild.classList.contains('private');
  }

  PRIVATE_PREVIEW_FIX() {
    unsafeWindow.$('img[alt!="Private"]').off('mouseover');
    unsafeWindow.$('img[alt!="Private"]').off('mouseout');
  }
}

//====================================================================================================

class VueApp {
  template = `
    <div class="fixed bottom-0 right-0 z-9999 rounded px-2 py-0.5 bg-zinc-800 max-w-full" v-if="state.uiEnabled">
    <div class="flex items-center cursor-pointer py-1 px-2 m-1" @click="state.hidden = !state.hidden">
      <span v-text="state.hidden ? '☒' : '⛶'" class="text-right w-full text-xl text-zinc-300"></span>
    </div>
    <template v-if="!state.hidden">
        <div class="flex items-center bg-zinc-900 py-1 px-2 m-1 font-mono">
          <input type="checkbox" id="exclude" v-model="state.filterNegative" class="mr-2 size-auto">
          <label for="exclude" class="text-zinc-300 font-mono">exclude</label>
          <input type="text" v-model="state.filterNegativeTags" placeholder="tag1, tag2,.." class="w-full h-8 text-white px-3 py-2 focus:outline-none bg-zinc-700 mx-2 rounded-sm">
        </div>
        <div class="flex items-center bg-zinc-900 py-1 px-2 m-1 font-mono">
          <input type="checkbox" id="include" v-model="state.filterPositive" class="mr-2 size-auto">
          <label for="include" class="text-zinc-300 font-mono">include</label>
          <input type="text" v-model="state.filterPositiveTags" placeholder="tag1, tag2,.." class="w-full h-8 text-white px-3 py-2 focus:outline-none bg-zinc-700 mx-2 rounded-sm size-auto">
        </div>
        <div class="flex items-center bg-zinc-900 py-1 px-2 m-1 font-mono">
          <input type="checkbox" id="filterPrivate" v-model="state.filterPrivate" class="mr-2 size-auto">
          <label for="filterPrivate" class="text-zinc-300 font-mono">private</label>
          <input type="checkbox" id="filterPublic" v-model="state.filterPublic" class="mx-2 size-auto">
          <label for="filterPublic" class="text-zinc-300 font-mono">public</label>
          <span v-if="state.pagIndexLast > 1" class="text-zinc-300 ml-auto">
            {{ state.pagIndexCur }}/{{ state.pagIndexLast }}
          </span>
        </div>
        <div class="flex items-center bg-zinc-900 py-1 px-2 m-1 font-mono">
          <input type="checkbox" id="duration" v-model="state.filterDuration" class="mr-2">
          <label for="duration" class="text-zinc-300 font-mono">duration</label>
          <label for="durationFrom" class="text-zinc-300 mx-2 font-mono">from</label>
          <input type="number" step="10" min="0" max="72000" id="durationFrom" v-model.number="state.filterDurationFrom" class="w-24 h-8 bg-gray-700 text-zinc-300 rounded px-3 py-2 focus:outline-none">
          <label for="durationTo" class="text-zinc-300 mx-2 font-mono">to</label>
          <input type="number" step="10" min="0" max="72000" id="durationTo" v-model.number="state.filterDurationTo" class="w-24 h-8 bg-gray-700 text-zinc-300 rounded px-3 py-2 focus:outline-none">
        </div>
    </template>
  </div>
  `;

  constructor(state) {
    const root = parseDOM('<div id="tapermonkey-app" style="position: relative; z-index: 999999;"></div>');
    document.body.appendChild(root);

    this.vue = createApp({
      setup: () => ({ state }),
      template: this.template
    }).mount("#tapermonkey-app");

    GM_addStyle(`#tapermonkey-app *,#tapermonkey-app :before,#tapermonkey-app :after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}#tapermonkey-app :before,#tapermonkey-app :after{--tw-content: ""}#tapermonkey-app html,#tapermonkey-app :host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}#tapermonkey-app body{margin:0;line-height:inherit}#tapermonkey-app hr{height:0;color:inherit;border-top-width:1px}#tapermonkey-app abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}#tapermonkey-app h1,#tapermonkey-app h2,#tapermonkey-app h3,#tapermonkey-app h4,#tapermonkey-app h5,#tapermonkey-app h6{font-size:inherit;font-weight:inherit}#tapermonkey-app a{color:inherit;text-decoration:inherit}#tapermonkey-app b,#tapermonkey-app strong{font-weight:bolder}#tapermonkey-app code,#tapermonkey-app kbd,#tapermonkey-app samp,#tapermonkey-app pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}#tapermonkey-app small{font-size:80%}#tapermonkey-app sub,#tapermonkey-app sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}#tapermonkey-app sub{bottom:-.25em}#tapermonkey-app sup{top:-.5em}#tapermonkey-app table{text-indent:0;border-color:inherit;border-collapse:collapse}#tapermonkey-app button,#tapermonkey-app input,#tapermonkey-app optgroup,#tapermonkey-app select,#tapermonkey-app textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}#tapermonkey-app button,#tapermonkey-app select{text-transform:none}#tapermonkey-app button,#tapermonkey-app input:where([type=button]),#tapermonkey-app input:where([type=reset]),#tapermonkey-app input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}#tapermonkey-app :-moz-focusring{outline:auto}#tapermonkey-app :-moz-ui-invalid{box-shadow:none}#tapermonkey-app progress{vertical-align:baseline}#tapermonkey-app ::-webkit-inner-spin-button,#tapermonkey-app ::-webkit-outer-spin-button{height:auto}#tapermonkey-app [type=search]{-webkit-appearance:textfield;outline-offset:-2px}#tapermonkey-app ::-webkit-search-decoration{-webkit-appearance:none}#tapermonkey-app ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}#tapermonkey-app summary{display:list-item}#tapermonkey-app blockquote,#tapermonkey-app dl,#tapermonkey-app dd,#tapermonkey-app h1,#tapermonkey-app h2,#tapermonkey-app h3,#tapermonkey-app h4,#tapermonkey-app h5,#tapermonkey-app h6,#tapermonkey-app hr,#tapermonkey-app figure,#tapermonkey-app p,#tapermonkey-app pre{margin:0}#tapermonkey-app fieldset{margin:0;padding:0}#tapermonkey-app legend{padding:0}#tapermonkey-app ol,#tapermonkey-app ul,#tapermonkey-app menu{list-style:none;margin:0;padding:0}#tapermonkey-app dialog{padding:0}#tapermonkey-app textarea{resize:vertical}#tapermonkey-app input::-moz-placeholder,#tapermonkey-app textarea::-moz-placeholder{opacity:1;color:#9ca3af}#tapermonkey-app input::placeholder,#tapermonkey-app textarea::placeholder{opacity:1;color:#9ca3af}#tapermonkey-app button,#tapermonkey-app [role=button]{cursor:pointer}#tapermonkey-app :disabled{cursor:default}#tapermonkey-app img,#tapermonkey-app svg,#tapermonkey-app video,#tapermonkey-app canvas,#tapermonkey-app audio,#tapermonkey-app iframe,#tapermonkey-app embed,#tapermonkey-app object{display:block;vertical-align:middle}#tapermonkey-app img,#tapermonkey-app video{max-width:100%;height:auto}#tapermonkey-app [hidden]{display:none}#tapermonkey-app *,#tapermonkey-app :before,#tapermonkey-app :after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }#tapermonkey-app ::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }#tapermonkey-app .fixed{position:fixed}#tapermonkey-app .bottom-0{bottom:0}#tapermonkey-app .right-0{right:0}#tapermonkey-app .m-1{margin:.25rem}#tapermonkey-app .mx-2{margin-left:.5rem;margin-right:.5rem}#tapermonkey-app .ml-auto{margin-left:auto}#tapermonkey-app .mr-2{margin-right:.5rem}#tapermonkey-app .flex{display:flex}#tapermonkey-app .hidden{display:none}#tapermonkey-app .size-auto{width:auto;height:auto}#tapermonkey-app .h-8{height:2rem}#tapermonkey-app .w-24{width:6rem}#tapermonkey-app .w-full{width:100%}#tapermonkey-app .cursor-pointer{cursor:pointer}#tapermonkey-app .resize{resize:both}#tapermonkey-app .flex-wrap{flex-wrap:wrap}#tapermonkey-app .items-center{align-items:center}#tapermonkey-app .rounded{border-radius:.25rem}#tapermonkey-app .rounded-sm{border-radius:.125rem}#tapermonkey-app .border{border-width:1px}#tapermonkey-app .bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}#tapermonkey-app .bg-zinc-700{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity))}#tapermonkey-app .bg-zinc-800{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity))}#tapermonkey-app .bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity))}#tapermonkey-app .px-2{padding-left:.5rem;padding-right:.5rem}#tapermonkey-app .px-3{padding-left:.75rem;padding-right:.75rem}#tapermonkey-app .py-0{padding-top:0;padding-bottom:0}#tapermonkey-app .py-0\.5{padding-top:.125rem;padding-bottom:.125rem}#tapermonkey-app .py-1{padding-top:.25rem;padding-bottom:.25rem}#tapermonkey-app .py-2{padding-top:.5rem;padding-bottom:.5rem}#tapermonkey-app .text-right{text-align:right}#tapermonkey-app .font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}#tapermonkey-app .text-xl{font-size:1.25rem;line-height:1.75rem}#tapermonkey-app .text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}#tapermonkey-app .text-zinc-300{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}`);
  }
}

//====================================================================================================

(function () {
  'use strict';
  console.log(SponsaaLogo);

  class DomManager {
    constructor() {
      this.data = new Map();
      this.container = this.createThumbsContainer();
      this.lazyImgLoader = new LazyImgLoader((target, delazify) => {
        if (!this.isFiltered(target)) {
          delazify(target);
        }
      });
    }

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

    filterPositiveTags_ = (k, v, tags) => {
      const containTagsNot = tags.some(tag => !k.includes(tag));
      v.element.classList.toggle('filtered-tag-pos', state.filterPositive && containTagsNot);
    }

    filterNegativeTags_ = (k, v, tags) => {
      const containTags = tags.some(tag => k.includes(tag));
      v.element.classList.toggle('filtered-tag-neg', state.filterNegative && containTags);
    }

    filterPrivate_(v) {
      v.element.classList.toggle('filtered-private',
        state.filterPrivate && RULES.IS_PRIVATE(v.element));
    }

    filterPublic_(v) {
      v.element.classList.toggle('filtered-public',
        state.filterPublic && !RULES.IS_PRIVATE(v.element));
    }

    filterByDuration_ = (v) => {
      const notInRange = v.duration < state.filterDurationFrom || v.duration > state.filterDurationTo;
      v.element.classList.toggle('filtered-duration', state.filterDuration && notInRange);
    }

    filter_ = (filters, offset = 0) => {
      const runFilters = [];

      if (filters.filterPrivate) {
        runFilters.push((_, v) => this.filterPrivate_(v));
      }

      if (filters.filterPublic) {
        runFilters.push((_, v) => this.filterPublic_(v));
      }

      if (filters.filterDuration) {
        runFilters.push((_, v) => this.filterByDuration_(v));
      }

      if (filters.filterNegative) {
        const tags = stringToTags(state.filterNegativeTags);
        runFilters.push((k, v) => this.filterNegativeTags_(k, v, tags));
      }

      if (filters.filterPositive) {
        const tags = stringToTags(state.filterPositiveTags);
        runFilters.push((k, v) => this.filterPositiveTags_(k, v, tags));
      }

      let offset_counter = 1;
      for (const [k, v] of this.data.entries()) {
        offset_counter++;
        if (offset_counter > offset) {
          for (const rf of runFilters) {
            rf(k, v);
          }
        }
      }
    }

    filterAll = (offset) => {
      const applyFilters = {};
      const stateFilters = ['filterPrivate', 'filterPublic', 'filterDuration', 'filterPositive', 'filterNegative'];
      stateFilters.forEach(f => { if (state[f]) applyFilters[f] = state[f] });
      this.filter_(applyFilters, offset);
    }

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

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

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

      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: RULES.GET_VIDEO_DURATION(thumbElement)
        });

        const { img, imgSrc } = RULES.PREVIEW_IMG_DATA(thumbElement);

        img.classList.add('tracking');

        this.lazyImgLoader.lazify(thumbElement, img, imgSrc);

        container.appendChild(thumbElement);
      }

      this.filterAll(data_offset);
      mount.before(container);
    };
  }

  class PreviewManager {
    stopTick(el) {
      if (this.tick) this.tick.unsubscribe();
      el.src = el.getAttribute('lazy-loading');
    }

    setTick(callback) {
      this.tick = rxjs.interval(ANIMATION_DELAY).pipe(rxjs.startWith(0)).subscribe(callback);
    }

    animatePreview = (e) => {
      const { target: el, type } = e;
      if (el.tagName !== 'IMG' || !el.classList.contains('tracking')) return;
      this.stopTick(el);
      if (type === 'mouseover' || type === 'touchstart') {
        this.setTick(() => {
          el.src = RULES.ITERATE_PREVIEW_IMG(el.src);
        });
      }
    };

    listen(dom) {
      listenEvents(dom, ['mouseout', 'mouseover', 'touchstart', 'touchend'], this.animatePreview);
    }
  }

  const RULES = new THISVID_RULES();

  class PaginationPageManager {
    constructor() {
      if (!RULES.PAGE_HAS_VIDEO) return;
      handleLoadedHTML(document.body, RULES.PAGINATION || RULES.PAGE_HAS_VIDEO.parentElement);
      previewManager.listen((RULES.PAGINATION || RULES.PAGE_HAS_VIDEO).parentElement);
      state.pagIndexLast = RULES.PAGINATION_LAST;
      if (!RULES.PAGINATION) return;

      RULES.PAGINATION.style.opacity = 0;
      RULES.PRIVATE_PREVIEW_FIX();

      this.paginationGenerator = this.createNextPageGenerator();

      this.paginationObserver = Observer.observeWhile(RULES.PAGINATION,
        this.generatorConsume, SCROLL_RESET_DELAY);
    }

    generatorConsume = async () => {
      const {
        value: { url, offset } = {},
        done,
      } = this.paginationGenerator.next();
      if (!done) {
        console.log(url);
        const nextPageHTML = await fetchHtml(url);
        const prevScrollPos = document.documentElement.scrollTop;
        handleLoadedHTML(nextPageHTML, RULES.PAGINATION);
        state.pagIndexCur = offset;
        window.scrollTo(0, prevScrollPos);
      }
      return !this.generatorDone;
    }

    createNextPageGenerator() {
      const { offset, iteratable_url } = RULES.URL_DATA();
      state.pagIndexCur = offset;

      function* nextPageGenerator() {
        for (let c = offset + 1; c <= RULES.PAGINATION_LAST; c++) {
          const url = iteratable_url(c);
          yield { url, offset: c };
        }
      }

      return nextPageGenerator();
    }
  }

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

    route() {
      const { href } = window.location;
      const allowed_pagination = [
        /\.com\/$/,
        /\/(categories|tags?)\//,
        /\/?q=.*/,
        /\/((\w+-)?rated|(\w+-)?popular|(\w+-)?private|(\w+-)?newest|winners)\/$/,
        /\/members\/\d+\/\w+_videos\//
      ];

      if (allowed_pagination.some(r => r.test(href))) {
        this.handlePaginationPage();
      } else if (/\/members\/\d+\/$/.test(href)) {
        this.handleMemberPage();
        initFriendship();
      }
    }

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

    handleMemberPage() {
      const privates = $('#list_videos_private_videos_items');
      if (privates) {
        const mistakes = $$('#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 { state } = new PersistentState({
    filterDurationFrom: 0,
    filterDurationTo: 600,
    filterDuration: false,
    filterPrivate: false,
    filterPublic: false,
    filterNegativeTags: "",
    filterNegative: false,
    filterPositiveTags: "",
    filterPositive: false,
    uiEnabled: true,
    pagIndexLast: 1,
    pagIndexCur: 1,
  });

  const { filter_, handleLoadedHTML } = new DomManager();

  watch([() => state.filterDurationFrom, () => state.filterDurationTo], (a, b) => {
    state.filterDurationFrom = parseIntegerOr(a[0], b[0]);
    state.filterDurationTo = parseIntegerOr(a[1], b[1]);
    if (state.filterDuration) filter_({ filterDuration: true });
  });

  watch(() => state.filterDuration, () => filter_({ filterDuration: true }));

  watch(() => state.filterPrivate, () => filter_({ filterPrivate: true }));

  watch(() => state.filterPublic, () => filter_({ filterPublic: true }));

  watch(() => state.filterNegative, () => filter_({ filterNegative: true }));

  watch(() => state.filterNegativeTags, () => {
    if (state.filterNegative) filter_({ filterNegative: true });
  }, { deep: true });

  watch(() => state.filterPositive, () => filter_({ filterPositive: true }));

  watch(() => state.filterPositiveTags, () => {
    if (state.filterPositive) filter_({ filterPositive: true });
  }, { deep: true });

  const previewManager = new PreviewManager();
  const router = new Router();

  GM_addStyle('.filtered-private, .filtered-duration, .filtered-public, .filtered-tag-pos, .filtered-tag-neg { display: none !important; }');
})();