ThisVid.com Improved

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

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

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 Improved
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      3.9
// @description  Infinite scroll (optional). Lazy loading. 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
// @match        https://*.thisvid.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=thisvid.com
// @grant        GM_addStyle
// @grant        GM_download
// @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==
/* globals jQuery, Vue, rxjs */

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

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

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.callback = callback;
        this.observer = new IntersectionObserver(this.handleIntersection.bind(this));
    }

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

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

    handleIntersection(entries) {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                this.callback(entry.target);
            }
        }
    }

    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 = 'data-original';

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

    lazify(parent, img) {
        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_2";

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

const { state } = new PersistentState({
    filterDurationFrom: 0,
    filterDurationTo: 600,
    filterDuration: false,
    filterPrivate: false,
    filterPublic: false,
    filterNegativeTags: "",
    filterNegative: false,
    filterPositiveTags: "",
    filterPositive: false,
    uiEnabled: true,
    infiniteScrollEnabled: true
});

const stateLocale = reactive({
    pagIndexLast: 1,
    pagIndexCur: 1,
});

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 SCROLL_RESET_DELAY = 350;
const ANIMATION_DELAY = 750;

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

class THISVID_RULES {
    constructor() {
        this.PAGINATION = $('.pagination');
        this.PAGE_HAS_VIDEO = $('.tumbpu[title]');
        this.PAGINATION_LAST = this.PAGINATION ?
            parseInt($('.pagination-next').previousElementSibling.firstElementChild.textContent) : 1;
        this.IS_OTHER_MEMBER_PAGE = !$('.my-avatar') && /members\/\d+\/$/.test(window.location.href);
    }

    GET_THUMBS(html) {
        const thumbs = Array.from($$('.tumbpu[title]', html));
        return thumbs.filter(thumb => !thumb.parentElement.classList.contains('thumbs-photo'));
    }

    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);
        if (privateThumb) {
            const src = parseCSSUrl(privateThumb.style.background);
            privateThumb.removeAttribute('style');
            img.setAttribute('data-original', src);
        }
        img.removeAttribute('data-cnt');
        img.classList.remove('lazy-load');
        return img;
    }

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

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

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

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

const RULES = new THISVID_RULES();


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

function friend(id, i = 0) {
    return fetchText(FRIEND_REQUEST_URL(id))
        .then((text) => console.log(`friend request #${i} with /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=`;

function initFriendship() {
    if (!RULES.IS_OTHER_MEMBER_PAGE) return;

    createFriendButton();

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

    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));
            friends = (await Promise.all(pagesFetched)).flatMap(getUsers);
        } else {
            friends = getUsers(document.body);
        }
        await Promise.all(friends.map((fid, i) => friend(fid, i)));
    }

    function createFriendButton() {
        GM_addStyle('.buttons {display: flex; flex-wrap: wrap} .buttons * {align-self: center; padding: 3px; margin: 1px;}');
        const button = parseDOM('<button style="background: radial-gradient(red, blueviolet);">friend everyone</button>');
        $('.buttons').appendChild(button);
        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 });
    }
}

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

async function getUserData(id) {
    const url = id.includes('member') ? id : `/members/${id}/`;
    return fetchHtml(url).then(html => {
        const memberVideos = unsafeWindow.$(html).find("span:contains('Videos uploaded')").children();
        const privateVideosCount = parseInt(memberVideos[1].innerText);
        const publicVideosCount = parseInt(memberVideos[0].innerText);

        return {
            publicVideosCount,
            privateVideosCount
        };
    });
}

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

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

unsafeWindow.requestPrivateAccess = requestPrivateAccess;

const haveAccessColor = 'linear-gradient(90deg, #31623b, #212144)';
const haveNoAccessColor = 'linear-gradient(90deg, #462525, #46464a)';

function checkPrivateVidsAccess() {
    document.querySelectorAll('.tumbpu > .private').forEach(t => {
        const thumb = t.parentElement;
        const url = thumb.href;
        fetchHtml(url).then(html => {
            const holder = html.querySelector('.video-holder > p');
            const haveAccess = !holder;
            thumb.style.background = haveAccess ? haveAccessColor : haveNoAccessColor;
            thumb.querySelector('.title').innerText += haveAccess ? '✅' : '❌';
            const uploaderEl = holder ? holder.querySelector('a') : html.querySelector('a.author');
            const uploader = uploaderEl.href.replace(/.*\/(\d+)\/$/, (a, b) => b);
            thumb.querySelector('.title').appendChild(parseDOM(holder ?
                                                               `<span onclick="requestPrivateAccess(event, ${uploader});"> 🚑 ${uploaderEl.innerText}</span>` :
                                                               `<span> 💅🏿 ${uploaderEl.innerText}</span>`));
        });
    });
}

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

function downloader() {
    if (!location.pathname.startsWith('/videos/')) return;
    function download() {
        const video = document.querySelector('video');
        if (!video) {
            unsafeWindow.$('.fp-ui').click();
            setTimeout(download, 500);
            return;
        }
        const url = video.getAttribute('src');
        const name = document.querySelector('.headline').innerText + '.mp4';
        GM_download({ url, name }); // saveAs: false
    }
    const btn = unsafeWindow.$('<li><a href="#" style="text-decoration: none;font-size: 2rem;">📼</a></li>');
    unsafeWindow.$('.share_btn').after(btn);
    btn.on('click', download);
}

unsafeWindow.$(document).ready(downloader);

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

class VueApp {
    template = `
    <div class="fixed bottom-0 right-0 z-9999 rounded px-2 py-0.5 bg-zinc-800 max-w-full m-1 py-1" 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 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 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>
          <button type="button" @click="checkPrivateVidsAccess();" class="mx-2 size-auto bg-gray-700 text-zinc-300 rounded px-3 py-2">check privates access</button>
        </div>
        <div class="flex items-center bg-zinc-900 py-1 px-2 m-1 font-mono">
          <input type="checkbox" id="infiniteScrollEnabled" v-model="state.infiniteScrollEnabled" class="mr-2 size-auto">
          <label for="infiniteScrollEnabled" class="text-zinc-300 font-mono">infinite scroll</label>
          <span v-if="stateLocale.pagIndexLast > 1" class="text-zinc-300 ml-auto">
            {{ stateLocale.pagIndexCur }}/{{ stateLocale.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">
          <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">
        </div>
    </template>
  </div>
  `;

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

        this.vue = createApp({
            setup: () => ({ state, stateLocale, checkPrivateVidsAccess }),
            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}`);
    }
}

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

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

    dataFilters = {
        filterPublic: new class {
            tag = 'filtered-public';
            createFilter = () => {
                return (v) => [this.tag, !RULES.IS_PRIVATE(v.element) && state.filterPublic]
            }
        },
        filterPrivate: new class {
            tag = 'filtered-private';
            createFilter = () => {
                return (v) => [this.tag, RULES.IS_PRIVATE(v.element) && state.filterPrivate]
            }
        },
        filterDuration: new class {
            tag = 'filtered-duration';
            createFilter = () => {
                return (v) => {
                    const notInRange = v.duration < state.filterDurationFrom || v.duration > state.filterDurationTo;
                    return [this.tag, state.filterDuration && notInRange];
                }
            }
        },
        filterNegative: new class {
            tag = 'filtered-exclude';
            createFilter = () => {
                const tags = stringToTags(state.filterNegativeTags);
                return (v) => {
                    const containTags = tags.some(tag => v.title.includes(tag));
                    return [this.tag, state.filterNegative && containTags];
                }
            }
        },
        filterPositive: new class {
            tag = 'filtered-include';
            createFilter = () => {
                const tags = stringToTags(state.filterPositiveTags);
                return (v) => {
                    const containTagsNot = tags.some(tag => !v.title.includes(tag));
                    return [this.tag, state.filterPositive && containTagsNot];
                }
            }
        }
    }

    addFilterStyles() {
        const tags = Object.keys(this.dataFilters).map(k => `.${this.dataFilters[k].tag}`).join(',');
        GM_addStyle(`${tags} { display: none !important; }`);
    }

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

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

        for (const f of Object.keys(filters)) {
            runFilters.push(this.dataFilters[f].createFilter());
        }

        let offset_counter = 1;
        for (const v of this.data.values()) {
            offset_counter++;
            if (offset_counter > offset) {
                for (const rf of runFilters) {
                    const [tag, condition] = rf(v);
                    v.element.classList.toggle(tag, condition);
                }
            }
        }
    }

    filterAll = (offset) => {
        const applyFilters = Object.assign({}, ...Object.keys(this.dataFilters).map(f => ({ [f]: state[f] })));
        this.filter_(applyFilters, offset);
    }

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

    handleLoadedHTML = (html, mount) => {
        const thumbs = RULES.GET_THUMBS(html);

        const data_offset = this.data.size;

        for (const thumbElement of thumbs) {
            const url = thumbElement.getAttribute('href');
            if (!url || this.data.has(url)) continue;

            const { title, duration } = RULES.GET_THUMB_DATA(thumbElement);
            this.data.set(url, { element: thumbElement, title, duration });

            const img = RULES.PREVIEW_IMG_DATA(thumbElement);
            this.lazyImgLoader.lazify(thumbElement, img);
            img.classList.add('tracking');

            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) {
        this.tick?.unsubscribe();
        el.src = el.getAttribute('data-orig') || el.getAttribute("src");
    }

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

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

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

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

        RULES.PRIVATE_PREVIEW_FIX();

        this.paginationGenerator = this.createNextPageGenerator();

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

    generatorConsume = async () => {
        if (!state.infiniteScrollEnabled) return;
        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);
            stateLocale.pagIndexCur = offset;
            window.scrollTo(0, prevScrollPos);
        }
        return !this.generatorDone;
    }

    createNextPageGenerator() {
        const { offset, iteratable_url } = RULES.URL_DATA();
        stateLocale.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|popular|private|newest|winners|updates)\/(\d+\/)?$/,
            /\/members\/\d+\/\w+_videos\//
        ];

        const isPaginationPage = allowed_pagination.some(r => r.test(href));
        const isMemberPage = /\/members\/\d+\/$/.test(href);

        this.handleMessages();

        if (!(isPaginationPage || isMemberPage)) return;

        this.ui = new VueApp(state, stateLocale);

        if (isMemberPage) {
            this.handleMemberPage();
        } else {
            this.handlePaginationPage();
        }
    }

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

    handleMemberPage() {
        handleLoadedHTML(document.body, false);
        previewManager.listen(document.body);
        initFriendship();
    }

    handleMessages() {
        if (!/\/my_messages\//.test(window.location.href)) return;
        for (const member of $$('.user-avatar > a')) {
            getUserData(member.href).then(({ publicVideosCount, privateVideosCount }) => {
                if (privateVideosCount > 0) {
                    const succColor = 'linear-gradient(#2f6eb34f, #66666647)';
                    const failColor = 'linear-gradient(rgba(179, 47, 47, 0.31), rgba(102, 102, 102, 0.28))';
                    const success = !member.parentElement.nextElementSibling.innerText.includes('declined');
                    member.parentElement.parentElement.style.background = success ? succColor : failColor;
                }
                $('.user-comment p', member.parentElement.parentElement).innerText += `  |  videos: ${publicVideosCount} public, ${privateVideosCount} private`;
            })
        }
    }
}

const { filter_, handleLoadedHTML } = new DomManager();
const previewManager = new PreviewManager();
const router = new Router();