ThisVid.com Improved

Infinite scroll & lazy loading. Preview for private videos. Filter: duration, public/private, include/exclude terms. Mass friend request button.

As of 2024-04-19. See the latest version.

// ==UserScript==
// @name         ThisVid.com Improved
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      3.7.1
// @description  Infinite scroll & lazy loading. Preview for private videos. Filter: duration, public/private, include/exclude terms. Mass friend request button.
// @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(_ => _);
}

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.sync();
    this.watchPersistence();
  }

  sync() {
    this.trySetFromLocalStorage();
    //window.addEventListener('focus', this.trySetFromLocalStorage);
  }

  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) {
    try {
      return Array.from(
        el.querySelector('#list_members_friends_items')
          .querySelectorAll('.tumbpu'))
        .map(e => e.href.match(/\d+/)[0]);
    } catch (error) {
      return [];
    }
  }

  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) => `${window.location.origin}/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];
    friend(memberId);
    let friendsEl = $('#list_members_friends');
    if (!friendsEl) return;
    friendsEl = friendsEl.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 => `${window.location.origin}/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)));
  }

  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 = 350;
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() {
    const { 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 size-auto">
          <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);
        }
      });
      GM_addStyle('.filtered-duration, .filtered-private, .filtered-public, .filtered-include, .filtered-exclude { display: none !important; }');
    }

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

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

    filterNegativeTags_ = (k, v, tags) => {
      const containTags = tags.some(tag => k.includes(tag));
      v.element.classList.toggle('filtered-exclude', 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 stateFilters = ['filterPrivate', 'filterPublic', 'filterDuration', 'filterPositive', 'filterNegative'];
      const applyFilters = Object.assign({}, ...stateFilters.map(f => ({ [f]: state[f] })));
      this.filter_(applyFilters, offset);
    }

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

    handleLoadedHTML = (html, mount) => {
      const thumbs = $$('.tumbpu[title]', html);

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

        if (mount) this.container.appendChild(thumbElement);
      }

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

  class PreviewManager {
    constructor(delay = ANIMATION_DELAY) {
      this.delay = delay;
    }

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

    setTick(callback) {
      this.tick = rxjs.interval(this.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() {
      handleLoadedHTML(document.body, false);
      previewManager.listen(document.body);
      this.ui = new VueApp(state);
    }
  }

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