ThisVid.com Improved

Infinite scroll (optional). Preview for private videos. Filter: duration, public/private, include/exclude terms. Check access to private vids. Mass friend request button. Sorts messages. Download button 📼

// ==UserScript==
// @name         ThisVid.com Improved
// @namespace    http://tampermonkey.net/
// @version      5.2.2
// @license      MIT
// @description  Infinite scroll (optional). Preview for private videos. Filter: duration, public/private, include/exclude terms. Check access to private vids.  Mass friend request button. Sorts messages. Download button 📼
// @author       smartacephale
// @supportURL   https://github.com/smartacephale/sleazy-fork
// @match        https://*.thisvid.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=thisvid.com
// @grant        GM_addStyle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/billy-herrington-utils.umd.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jabroni-outfit.umd.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/lskdb.umd.js
// @run-at       document-idle
// ==/UserScript==
/* globals $ */

const {
  Tick,
  parseDom,
  fetchWith,
  fetchHtml,
  timeToSeconds,
  parseCSSUrl,
  circularShift,
  range,
  listenEvents,
  replaceElementTag,
  sanitizeStr,
  chunks,
  downloader,
  AsyncPool,
  computeAsyncOneAtTime,
  InfiniteScroller,
  DataManager,
} = window.bhutils;
Object.assign(unsafeWindow, { bhutils: window.bhutils });
const {
  JabroniOutfitStore,
  defaultStateWithDurationAndPrivacy,
  JabroniOutfitUI,
  defaultSchemeWithPrivateFilter,
} = window.jabronioutfit;
const { LSKDB } = window.lskdb;

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

GM_addStyle(`
  .haveNoAccess { background: linear-gradient(to bottom, #b50000 0%, #2c2c2c 100%) red !important; }
  .haveAccess { background: linear-gradient(to bottom, #4e9299 0%, #2c2c2c 100%) green !important; }
  .success { background: linear-gradient(#2f6eb34f, #66666647) !important; }
  .failure { background: linear-gradient(rgba(179, 47, 47, 0.31), rgba(102, 102, 102, 0.28)) !important; }
  .friend-button { background: radial-gradient(#5ccbf4, #e1ccb1) !important; }
  .friendProfile { background: radial-gradient(circle, rgb(28, 42, 50) 48%, rgb(0, 0, 0) 100%) !important; }
  `);

class THISVID_RULES {
  delay = 350;

  constructor() {
    const { href, pathname } = window.location;

    this._PAGINATION_ALLOWED = [
      /\.com\/$/,
      /\/(categories|tags?)\//,
      /\/?q=.*/,
      /\/(\w+-)?(rated|popular|private|newest|winners|updates)\/(\d+\/)?$/,
      /\/members\/\d+\/\w+_videos\//,
      /\/playlist\/\d+\//,
      /\/my_(\w+)_videos\//,
      /\/my_wall\/#\w+/,
    ].some((r) => r.test(href));

    this.IS_MEMBER_PAGE = /^\/members\/\d+\/$/.test(pathname);
    this.IS_WATCHLATER_KIND = /^\/my_(\w+)_videos\//.test(pathname);
    this.IS_MESSAGES_PAGE = /^\/my_messages\//.test(pathname);
    this.IS_PLAYLIST = /^\/playlist\/\d+\//.test(pathname);
    this.IS_VIDEO_PAGE = /^\/videos\//.test(pathname);

    this.PAGE_HAS_VIDEO = this.GET_THUMBS(document).length > 0;

    this.paginationElement = document.querySelector('.pagination');
    this.paginationLast = this.getPaginationLast();
    this.paginationOffset = this.getPaginationOffset();
    this.paginationUrlGenerator = this.getPaginationGenerator();

    this.CONTAINER = Array.from(document.querySelectorAll('.thumbs-items')).pop();

    this.MY_ID = document.querySelector('[target="_self"]')?.href.match(/\/(\d+)\//)[1] || null;
    this.LOGGED_IN = !!this.MY_ID;
    this.IS_MY_MEMBER_PAGE = this.LOGGED_IN && !!document.querySelector('.my-avatar');
    this.IS_OTHER_MEMBER_PAGE = !this.IS_MY_MEMBER_PAGE && this.IS_MEMBER_PAGE;
    this.IS_MEMBER_FRIEND =
      this.IS_OTHER_MEMBER_PAGE &&
      document.querySelector('.case-left')?.innerText.includes('is in your friends');

    if (this.IS_MEMBER_FRIEND) {
      document.querySelector('.profile').classList.add('friendProfile');
    }

    if (this.IS_PLAYLIST) {
      const videoUrl = this.PLAYLIST_THUMB_URL(pathname);
      const desc = document.querySelector('.tools-left > li:nth-child(4) > .title-description');
      const link = replaceElementTag(desc, 'a');
      link.href = videoUrl;
    }
  }

  getPaginationOffset() {
    return this.IS_PLAYLIST ? 1 : parseInt(location.pathname.match(/\/(\d+)\/?$/)?.[1]) || 1;
  }

  getPaginationLast(doc) {
    const e = doc || document;
    return parseInt(e.querySelector('.pagination-next')?.previousElementSibling?.innerText) || 1;
  }

  GET_THUMBS(html) {
    if (this.IS_WATCHLATER_KIND) {
      return Array.from(html.querySelectorAll('.thumb-holder'));
    }
    let thumbs = Array.from(html.querySelectorAll('.tumbpu[title]'));
    if (thumbs.length === 0 && html?.classList?.contains('tumbpu')) thumbs = [html];
    return thumbs.filter((thumb) => !thumb?.parentElement.classList.contains('thumbs-photo'));
  }

  PLAYLIST_THUMB_URL(src) {
    return src.replace(/playlist\/\d+\/video/, () => 'videos');
  }

  THUMB_URL(thumb) {
    if (this.IS_WATCHLATER_KIND) {
      return thumb.firstElementChild.href;
    }
    let url = thumb.getAttribute('href');
    if (this.IS_PLAYLIST) url = this.PLAYLIST_THUMB_URL(url);
    return url;
  }

  getPaginationGenerator(proxyLocation) {
    const url = new URL(proxyLocation || window.location);

    if (url.pathname === '/') url.pathname = '/latest-updates/';
    if (!/\/(\d+)\/?$/.test(url.pathname))
      url.pathname = `${url.pathname}${this.paginationOffset}/`;

    const paginationUrlGenerator = (n) => {
      if (this.IS_PLAYLIST) {
        url.search = `mode=async&function=get_block&block_id=playlist_view_playlist_view&sort_by=added2fav_date&from=${n}&_=${Date.now()}`;
      } else {
        url.pathname = url.pathname.replace(/\/\d+\/$/, `/${n}/`);
      }
      return url.href;
    };

    return paginationUrlGenerator;
  }

  THUMB_IMG_DATA(thumb) {
    const img = thumb.querySelector('img');
    const privateThumb = thumb.querySelector('.private');
    let imgSrc = img?.getAttribute('data-original');
    if (privateThumb) {
      imgSrc = parseCSSUrl(privateThumb.style.background);
      privateThumb.removeAttribute('style');
    }
    const count = img.getAttribute('data-cnt');
    if (!count || count === '6') img.removeAttribute('data-cnt');
    img.classList.remove('lazy-load');
    img.classList.add('tracking');

    if (this.IS_PLAYLIST) {
      img.onmouseover = img.onmouseout = null;
      img.removeAttribute('onmouseover');
      img.removeAttribute('onmouseout');
    }

    return { img, imgSrc };
  }

  THUMB_DATA(thumb) {
    const title = sanitizeStr(thumb.querySelector('.title').innerText);
    const duration = timeToSeconds(thumb.querySelector('.thumb > .duration').textContent);
    return { title, duration };
  }

  IS_PRIVATE(thumb) {
    return !thumb.querySelector('.private');
  }
}

const RULES = new THISVID_RULES();

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

function friend(id, message = '') {
  return fetchWith(FRIEND_REQUEST_URL(id, message));
}

const FRIEND_REQUEST_URL = (id, message = '') =>
  `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=${message}`;

const USERS_PER_PAGE = 24;

async function getMemberFriends(memberId, start, end) {
  const { friendsCount } = await getMemberData(memberId);
  const offset = Math.ceil(friendsCount / USERS_PER_PAGE);
  const pages = range(offset)
    .slice(start, end)
    .map((o) => `https://thisvid.com/members/${memberId}/friends/${o}/`);
  const pagesFetched = pages.map((p) => fetchHtml(p));
  const friends = (await Promise.all(pagesFetched)).flatMap(getMembers);
  return friends;
}

function getMembers(el) {
  const friendsList = el.querySelector('#list_members_friends_items');
  return Array.from(friendsList?.querySelectorAll('.tumbpu') || [])
    .map((e) => e.href.match(/\d+/)?.[0])
    .filter((_) => _);
}

async function friendMemberFriends(orientationFilter) {
  const memberId = window.location.pathname.match(/\d+/)[0];
  friend(memberId);
  const friends = await getMemberFriends(memberId);
  const spool = new AsyncPool(60);
  friends
    .map((fid) => {
      if (!orientationFilter) return () => friend(fid);
      return () =>
        getMemberData(fid).then(async ({ orientation, uploadedPrivate }) => {
          if (
            uploadedPrivate > 0 &&
            (orientation === orientationFilter ||
              (orientationFilter === 'Straight' && orientation === 'Lesbian'))
          ) {
            await friend(fid);
          }
        });
    })
    .forEach((f) => spool.push(f));
  await spool.run();
}

function initFriendship() {
  GM_addStyle(
    '.buttons {display: flex; flex-wrap: wrap} .buttons button, .buttons a {align-self: center; padding: 4px; margin: 5px;}',
  );

  const buttonAll = parseDom(
    '<button style="background: radial-gradient(red, blueviolet);">friend everyone</button>',
  );
  const buttonStraightOnly = parseDom(
    '<button style="background: radial-gradient(red, #a18cb5);">friend straights</button>',
  );
  const buttonGayOnly = parseDom(
    '<button style="background: radial-gradient(red, #46baff);">friend gays</button>',
  );
  const buttonBisexualOnly = parseDom(
    '<button style="background: radial-gradient(red, #4ebaaf);">friend bisexuals</button>',
  );

  document
    .querySelector('.buttons')
    .append(buttonAll, buttonStraightOnly, buttonGayOnly, buttonBisexualOnly);

  buttonAll.addEventListener('click', (e) => handleClick(e), { once: true });
  buttonStraightOnly.addEventListener('click', (e) => handleClick(e, 'Straight'), { once: true });
  buttonGayOnly.addEventListener('click', (e) => handleClick(e, 'Gay'), { once: true });
  buttonBisexualOnly.addEventListener('click', (e) => handleClick(e, 'Bisexual'), { once: true });

  function handleClick(e, orientationFilter) {
    const button = e.target;
    button.style.background = 'radial-gradient(#ff6114, #5babc4)';
    button.innerText = 'processing requests';
    friendMemberFriends(orientationFilter).then(() => {
      button.style.background = 'radial-gradient(blue, lightgreen)';
      button.innerText = 'friend requests sent';
    });
  }
}

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

async function sendMessage(uid, message = 'add me pls') {
  const url = new URL(
    `https://thisvid.com/members/${uid}/?action=send_message_complete&function=get_block&block_id=member_profile_view_view_profile&format=json&mode=async`,
  );
  url.searchParams.append('message', message);
  await fetch(url.href);
}

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

async function getMemberData(id) {
  const url = id.includes('member') ? id : `/members/${id}/`;
  const doc = await fetchHtml(url);
  const data = {};

  doc.querySelectorAll('.profile span').forEach((s) => {
    if (s.innerText.includes('Name:')) {
      data.name = s.firstElementChild.innerText.trim();
    }
    if (s.innerText.includes('Orientation:')) {
      data.orientation = s.firstElementChild.innerText.trim();
    }
    if (s.innerText.includes('Videos uploaded:')) {
      data.uploadedPublic = parseInt(s.children[0].innerText);
      data.uploadedPrivate = parseInt(s.children[1].innerText);
    }
  });

  data.friendsCount =
    parseInt(
      doc.querySelector('#list_members_friends')?.firstElementChild.innerText.match(/\d+/g).pop(),
    ) || 0;

  return data;
}

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

unsafeWindow.requestPrivateAccess = (e, memberid) => {
  e.preventDefault();
  friend(memberid, '');
  e.target.innerText = e.target.innerText.replace('🚑', '🍆');
};

async function checkPrivateVideoAccess(url) {
  const html = await fetchHtml(url);
  const holder = html.querySelector('.video-holder > p');

  const access = !holder;

  const uploaderEl = holder ? holder.querySelector('a') : html.querySelector('a.author');
  const uploaderURL = uploaderEl.href.match(/\d+/).at(-1);
  const uploaderName = uploaderEl.innerText;

  return {
    access,
    uploaderURL,
    uploaderName,
  };
}

const uploadersNotInFriendlist = new Set();

async function requestAccess() {
  const checkAccess = async (thumb) => {
    const { access, uploaderURL } = await checkPrivateVideoAccess(thumb.querySelector('a').href);

    if (!access) {
      thumb.classList.add('haveNoAccess');

      if (!uploadersNotInFriendlist.has(uploaderURL)) friend(uploaderURL);
    } else {
      thumb.classList.add('haveAccess');
    }
  };

  const f = [];
  document
    .querySelectorAll('.tumbpu:has(.private), .thumb-holder:has(.private)')
    .forEach((thumb) => {
      if (!thumb.classList.contains('haveNoAccess') && !thumb.classList.contains('haveAccess')) {
        f.push(() => checkAccess(thumb));
      }
    });
  computeAsyncOneAtTime(f);
}

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

const createDownloadButton = () =>
  downloader({
    append: '',
    after: '.share_btn',
    button: '<li><a href="#" style="text-decoration: none;font-size: 2rem;">📼</a></li>',
    cbBefore: () => $('.fp-ui').click(),
  });

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

class PreviewAnimation {
  constructor(element, delay = 750) {
    $('img[alt!="Private"]').off();
    this.tick = new Tick(delay);
    listenEvents(element, ['mouseover', 'touchstart'], this.animatePreview);
  }

  ITERATE_PREVIEW_IMG = (img) => {
    const count = parseInt(img.getAttribute('data-cnt')) || 6;
    img.src = img
      .getAttribute('src')
      .replace(/(\d+)(?=\.jpg$)/, (_, n) => `${circularShift(parseInt(n), count)}`);
  };

  animatePreview = (e) => {
    const { target: el, type } = e;
    if (!el.classList.contains('tracking') || !el.getAttribute('src')) return;
    this.tick.stop();
    if (type === 'mouseover' || type === 'touchstart') {
      const orig = el.getAttribute('src');
      this.tick.start(
        () => this.ITERATE_PREVIEW_IMG(el),
        () => {
          el.src = orig;
        },
      );
      el.addEventListener(
        type === 'mouseover' ? 'mouseleave' : 'touchend',
        () => this.tick.stop(),
        { once: true },
      );
    }
  };
}

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

function highlightMessages() {
  for (const member of document.querySelectorAll('.user-avatar > a')) {
    getMemberData(member.href).then(({ uploadedPublic, uploadedPrivate }) => {
      if (uploadedPrivate > 0) {
        const success = !member.parentElement.nextElementSibling.innerText.includes('declined');
        member.parentElement.parentElement.classList.add(success ? 'success' : 'failure');
      }
      member.parentElement.parentElement.querySelector('.user-comment p').innerText +=
        `  |  videos: ${uploadedPublic} public, ${uploadedPrivate} private`;
    });
  }
}

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

const lskdb = new LSKDB();

async function getMemberVideos(id, type = 'private') {
  const { uploadedPrivate, uploadedPublic, name } = await getMemberData(id);
  const videosCount = type === 'private' ? uploadedPrivate : uploadedPublic;
  const paginationLast = Math.ceil(videosCount / 48);

  const iteratable_url = RULES.getPaginationGenerator(
    new URL(`https://thisvid.com/members/${id}/${type}_videos/`),
  );
  const memberVideosGenerator = InfiniteScroller.createPaginationGenerator(
    0,
    paginationLast,
    iteratable_url,
  );
  return { name, videosCount, memberVideosGenerator };
}

async function getMembersVideos(id, friendsCount, memberGeneratorCallback, type = 'private') {
  let skipFlag = false;
  let skipCount = 1;
  let minVideosCount = 1;

  const skipCurrentMember = (n = 1) => {
    skipFlag = true;
    skipCount = n;
  };
  const filterVideosCount = (n = 1) => {
    minVideosCount = n;
  };

  let membersIds = await getMemberFriends(id, 0, 1);
  getMemberFriends(id, 1).then((r) => {
    membersIds = membersIds.concat(r);
  });

  async function* pageGenerator() {
    let currentGenerator = null;
    for (let c = 0; c < friendsCount - 1; c++) {
      if (lskdb.hasKey(membersIds[c])) continue;

      if (!currentGenerator) {
        const { memberVideosGenerator, name, videosCount } = await getMemberVideos(
          membersIds[c],
          type,
        );

        if (memberVideosGenerator && videosCount >= minVideosCount) {
          currentGenerator = memberVideosGenerator;
          memberGeneratorCallback(name, videosCount, membersIds[c]);
        } else continue;
      }

      const {
        value: { url } = {},
        done,
      } = await currentGenerator.next();

      if (done || skipFlag) {
        c += skipCount - 1;
        skipCount = 1;
        currentGenerator = null;
        skipFlag = false;
      } else {
        yield { url, offset: c };
      }
    }
  }

  return {
    pageGenerator: () => pageGenerator(membersIds, type),
    skipCurrentMember,
    filterVideosCount,
  };
}

function createPrivateFeedButton() {
  const container = document.querySelectorAll('.sidebar ul')[1];
  const buttonPrv = parseDom(
    `<li><a href="https://thisvid.com/my_wall/#private_feed" class="selective"><i class="ico-arrow"></i>My Friends Private Videos</a></li>`,
  );
  const buttonPub = parseDom(
    `<li><a href="https://thisvid.com/my_wall/#public_feed" class="selective"><i class="ico-arrow"></i>My Friends Public Videos</a></li>`,
  );
  container.append(buttonPub, buttonPrv);
}

async function createPrivateFeed() {
  createPrivateFeedButton();
  if (!window.location.hash.includes('feed')) return;
  const isPubKey = window.location.hash === '#public_feed';

  const container = parseDom('<div class="thumbs-items"></div>');
  const ignored = parseDom('<div class="ignored"><h2>IGNORED:</h2></div>');

  Object.assign(defaultSchemeWithPrivateFilter, {
    controlsSkip: [
      { type: 'button', innerText: 'skip 10', callback: async () => skip(10) },
      { type: 'button', innerText: 'skip 100', callback: async () => skip(100) },
      { type: 'button', innerText: 'skip 1000', callback: async () => skip(1000) },
    ],
    controlsFilter: [
      { type: 'button', innerText: 'filter >10', callback: async () => filterVidsCount(10) },
      { type: 'button', innerText: 'filter >25', callback: async () => filterVidsCount(25) },
      { type: 'button', innerText: 'filter >100', callback: async () => filterVidsCount(100) },
    ],
  });

  const containerParent = document.querySelector('.main > .container > .content');
  containerParent.innerHTML = '';
  containerParent.nextElementSibling.remove();
  containerParent.append(container);
  container.before(ignored);
  GM_addStyle(`.content { width: auto; }
   .member-videos, .ignored { background: #b3b3b324; min-height: 3rem; margin: 1rem 0px; color: #fff; font-size: 1.24rem; display: flex; flex-wrap: wrap; justify-content: center;
     padding: 10px; width: 100%; }
   .member-videos * {  padding: 5px; margin: 4px; }
   .member-videos h2 a { font-size: 1.24rem; margin: 0; padding: 0; display: inline; }
   .ignored * {  padding: 4px; margin: 5px; }
   .thumbs-items { display: flex; flex-wrap: wrap; }`);

  RULES.intersectionObservable = document.querySelector('.footer');
  RULES.CONTAINER = container;

  const { friendsCount } = await getMemberData(RULES.MY_ID);

  RULES.paginationLast = friendsCount;

  const { pageGenerator, skipCurrentMember, filterVideosCount } = await getMembersVideos(
    RULES.MY_ID,
    friendsCount,
    (name, videosCount, id) => {
      container.append(
        parseDom(`
       <div class="member-videos" id="mem-${id}">
         <h2><a href="/members/${id}/">${name}</a> ${videosCount} videos</h2>
         <button onClick="hideMemberVideos(event)">ignore 🗡</button>
         <button onClick="hideMemberVideos(event, false)">skip</button>
       </div>`),
      );
    },
    isPubKey ? 'public' : 'private',
  );

  RULES.alternativeGenerator = pageGenerator;

  const ignoredMembers = lskdb.getAllKeys();
  ignoredMembers.forEach((im) => {
    document
      .querySelector('.ignored')
      .append(parseDom(`<button id="#ir-${im}" onClick="unignore(event)">${im} 🗡</button>`));
  });

  const skip = (n) => {
    skipCurrentMember(n);
    document.querySelector('.thumbs-items').innerHTML = '';
  };

  unsafeWindow.hideMemberVideos = (e, ignore = true) => {
    let id = e.target.parentElement.id;
    if (!document.querySelector(`#${id} ~ div`)) {
      skipCurrentMember();
    }
    const box = document.getElementById(id);
    const toDelete = [box];
    let curr = box.nextElementSibling;
    while (curr?.classList.contains('tumbpu')) {
      toDelete.push(curr);
      curr = curr.nextElementSibling;
    }
    toDelete.forEach((e) => e.remove());
    id = id.slice(4);
    if (ignore) {
      document
        .querySelector('.ignored')
        .append(parseDom(`<button id="irm-${id}" onClick="unignore(event)">${id} X</button>`));
      lskdb.setKey(id);
    }
  };

  unsafeWindow.unignore = (e) => {
    const id = e.target.id.slice(4);
    lskdb.removeKey(id);
    e.target.remove();
  };

  const filterVidsCount = (count) => filterVideosCount(count);

  initInfiniteScroll();
}

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

async function clearMessages() {
  const sortMsgs = (doc) => {
    doc.querySelectorAll('.entry').forEach((e) => {
      const id = e.querySelector('input[name="delete[]"]').value;
      const msg = e.querySelector('.user-comment').innerText;
      if (/has confirmed|declined your|has removed/g.test(msg)) deleteMsg(id);
    });
  };

  const deleteMsg = (id) => {
    const url = `https://thisvid.com/my_messages/inbox/?mode=async&format=json&action=delete&function=get_block&block_id=list_messages_my_conversation_messages&delete[]=${id}`;
    fetch(url).then((res) => console.log(url, res?.status));
  };

  await Promise.all(
    Array.from({ length: RULES.paginationLast }, (_, i) =>
      fetchHtml(`https://thisvid.com/my_messages/inbox/${i + 1}/`).then((html) => sortMsgs(html)),
    ),
  );
}

function clearMessagesButton() {
  const btn = parseDom('<button>clear messages</button>');
  btn.addEventListener('click', clearMessages);
  document.querySelector('.headline').append(btn);
}

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

function initInfiniteScroll() {
  const iscroller = new InfiniteScroller({
    enabled: state.infiniteScrollEnabled,
    handleHtmlCallback: handleLoadedHTML,
    ...RULES,
  }).onScroll((i) => {
    stateLocale.pagIndexLast = i.paginationLast;
    stateLocale.pagIndexCur = i.paginationOffset;
  });

  store.subscribe(() => {
    iscroller.enabled = state.infiniteScrollEnabled;
  });

  return iscroller;
}

function route() {
  console.log(SponsaaLogo);

  if (RULES.LOGGED_IN) {
    defaultSchemeWithPrivateFilter.privateFilter.push({
      type: 'button',
      innerText: 'request access 🔓',
      callback: requestAccess,
    });
  }

  if (RULES.IS_MY_MEMBER_PAGE) {
    createPrivateFeed();
    RULES.PAGE_HAS_VIDEO = true;
  }

  if (RULES.IS_MESSAGES_PAGE) {
    clearMessagesButton();
    highlightMessages();
  }

  if (RULES.IS_VIDEO_PAGE) {
    const holder = document.querySelector('.video-holder > p');
    if (holder) {
      const uploader = document.querySelector('a.author').href.match(/\d+/).at(-1);
      holder.parentElement.append(
        parseDom(
          `<button onclick="requestPrivateAccess(event, ${uploader}); this.onclick=null;">Friend Request</button>`,
        ),
      );
    }
    createDownloadButton();
  }

  if (!RULES.PAGE_HAS_VIDEO) return;

  const containers = Array.from(
    RULES.IS_WATCHLATER_KIND
      ? [RULES.CONTAINER]
      : document.querySelectorAll('.thumbs-items:not(.thumbs-members)'),
  );

  if (containers.length > 1 && !RULES.IS_MEMBER_PAGE) RULES.CONTAINER = containers[0];
  containers.forEach((c) => {
    handleLoadedHTML(c, RULES.IS_MEMBER_PAGE ? c : RULES.CONTAINER, true);
  });

  new PreviewAnimation(document.body);
  new JabroniOutfitUI(store, defaultSchemeWithPrivateFilter);

  if (RULES.IS_OTHER_MEMBER_PAGE) {
    initFriendship();
  }

  if (RULES._PAGINATION_ALLOWED) {
    stateLocale.pagIndexLast = RULES.paginationLast;
    if (!RULES.paginationElement) return;
    initInfiniteScroll();
  }
}

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

const store = new JabroniOutfitStore(defaultStateWithDurationAndPrivacy);
const { state, stateLocale } = store;
const { applyFilters, handleLoadedHTML } = new DataManager(RULES, state);
store.subscribe(applyFilters);

route();