// ==UserScript==
// @name XHamster Improved
// @namespace http://tampermonkey.net/
// @license MIT
// @version 1.3
// @description Infinite scroll. Lazy loading. Filter by duration, include/exclude phrases. Automatically expand more videos on video page.
// @author smartacephale
// @match https://*.xhamster.com/*
// @match https://*.xhamster.desi/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=xhamster.com
// @require https://unpkg.com/[email protected]/dist/vue.global.prod.js
// @require https://update.greasyfork.org/scripts/494103/utils.user.js
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
/* globals jQuery, $, Vue */
const LOGO = `
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡘⢲⣃⢖⡚⡴⢣⡞⠰⠁⡀⠀⠀⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⢮⡵⣫⣝⡳⠏⢠⢃⡐⠁⡘⢀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⡀⠀⡀⠀⠀⠀⠀⢀⠐⡀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠔⡉⢃⡉⠓⡈⢃⠊⡐⠡⡐⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠠⠐⠀⠄⠐⠀⡐⠀⠠⠁⡀⢁⠀⠂⠄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠂⡔⢢⡐⢣⡜⢤⠣⡜⡰⢀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠠⠐⠀⠠⠐⡈⠄⡈⠐⡀⠌⢀⠐⠀⠄⡈⢂⠔⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠦⡙⣌⠣⡜⢣⡚⡥⣛⠴⣡⠃⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠐⠈⢀⠐⠈⡐⠠⠐⠠⢀⠡⠐⡀⠂⠌⡐⠠⢐⠡⢈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠈⡐⠠⣉⠖⡱⢌⠳⣌⢣⡑⠦⠡⢏⡴⣉⠂⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠀⠐⠀⠠⢁⠀⢂⠁⠂⠄⠂⡁⠄⢁⠂⠄⡁⢢⠘⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠈⠆⢀⠰⣀⠣⢔⡩⢜⢪⡱⢂⠇⡌⢣⠁⡞⣰⠡⢂⠀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠁⠂⠈⠀⠀⠈⡀⠌⠐⠈⠐⠀⠈⠄⢀⠂⠐⡀⢊⠡⠀⠀⠀⠀⠀⠀⠀⠀⠠⡈⢆⡹⠀⢂⢅⠢⣍⠒⡜⣬⠣⣕⠫⡜⢠⠃⢰⠩⡖⡑⢂⠀⡀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⠀⠈⠀⠄⠀⠄⠂⢀⠈⠀⠀⠀⠀⠁⡀⠣⢘⠀⠀⠀⠀⠀⠀⠀⠄⡑⠌⢢⠐⡡⢌⡊⠵⡨⠝⢲⡤⡛⣌⠳⣈⠆⠡⢈⠳⣌⡑⢂⠠⠀⠂⠀⢀⠀⠀⢄⡀⡠⠀⠄⠀⠀
⠀⠀⠀⠀⠀⠀⠀⡀⠀⢈⠀⠂⠁⡀⠠⠀⡀⢈⢄⡒⡔⣢⢔⢣⢆⢆⠤⣠⢀⡀⣀⠬⣰⠜⣎⢧⢯⡵⣫⣜⣳⣙⣎⢧⡒⡥⢌⡱⢀⠂⠁⢈⠱⡰⢌⠂⠤⢑⡈⢅⠢⢌⡑⢆⠱⣡⠋⡔⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢀⠐⠀⢀⠂⢀⠀⡄⢒⡌⠶⣘⠶⣙⢦⣛⢮⡞⣎⡗⣦⠓⡴⣡⢟⣵⡻⣽⡞⣷⢯⣗⣻⢶⣫⡞⣶⡹⣜⣣⢖⡡⢆⡀⢀⠃⠖⡨⠐⣈⠦⡘⢤⠣⠜⡌⣌⠢⢱⡘⠄⠐⠈
⠀⠀⠀⠀⠀⠀⠈⠀⠀⠐⠀⢀⠢⣘⠰⣃⠞⣵⢫⡟⣽⣺⢽⣮⡝⡾⡼⣉⢾⣱⢯⣟⡾⣽⣳⢿⣽⣻⢾⣽⣳⢯⣽⣳⢯⣳⢳⣎⡗⢮⡐⢆⡉⢒⠡⠀⢖⡰⢉⢆⠣⣍⠲⣀⠣⣁⠎⠄⠀⢀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢆⡱⢌⡳⢭⡞⣭⢷⣻⢷⣯⣟⡾⣽⣳⡱⣝⡮⣟⣽⢾⣻⣽⣟⣯⣷⣿⣻⣾⣽⣻⡾⣽⢯⣟⡷⣞⡽⢮⣝⠲⣌⠢⢁⠂⠥⢊⡕⣊⠱⣂⠓⡄⠣⡔⡈⠀⠀⢀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣍⠲⣘⢮⡱⣏⡾⣽⢯⣟⣿⣾⡽⣿⡳⢧⡳⣝⡾⣽⢯⣿⣟⣯⣿⣿⣻⣾⣟⣷⣿⢯⣿⢯⣿⢾⣽⣻⣞⡷⣎⢿⡰⣃⠆⡈⢰⢣⡘⢤⠓⣌⢣⠐⡡⢒⠀⠁⠀⢈
⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⠤⣃⠳⣎⣳⣭⢿⣽⣻⡾⣿⣟⣿⢻⣝⡣⡝⣮⢿⣽⣻⣯⣿⣿⣿⣿⣿⣿⣿⣿⣻⣿⢿⣻⣯⣿⣞⣷⣻⣼⣛⡮⣵⢣⡚⢄⠘⢆⡙⢦⡙⢤⠃⢎⠡⠆⡌⠀⠀⠠
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡴⣉⠗⣎⢷⣚⡿⣞⣷⣻⢷⡯⣝⠳⣪⠵⡹⣜⣻⢾⣽⣻⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣟⣷⡿⣾⣳⣟⣞⣳⡽⣲⢣⡝⢢⠌⡠⢈⠂⠝⣢⠹⣌⠱⢌⠄⠀⠁⠀
⢣⠜⡄⢦⢀⠤⢂⠔⠀⠑⡰⢡⡛⣬⢳⢯⡽⣻⡼⢯⣳⠽⡌⢯⡔⣫⢷⡸⣝⠿⣞⡿⣯⣿⣿⡿⣿⣿⢿⣿⣽⣷⣿⣿⣻⣽⡷⣟⡾⣝⣧⢻⡱⢇⡞⡡⢎⠔⡡⢌⡐⠠⠑⠌⡱⢈⠤⠈⠄⣀
⣂⠎⡰⢢⠍⢆⠃⢠⢩⡔⣮⠱⡸⣌⢗⣫⢞⡵⣻⢏⡧⣏⡽⣌⠲⣭⢖⡧⣫⢟⡽⣻⡽⣷⣻⢿⣻⣽⡿⣯⣿⢯⣷⣿⣻⣽⣻⣽⣛⠷⣎⠷⣙⠮⣜⡱⣊⡜⣡⠒⣌⠱⠈⠄⠡⢂⠒⡌⢰⠀
⢍⡶⣙⢦⡙⢆⠈⣴⢣⡿⣜⡷⣥⠊⢞⡰⢫⡜⣣⠏⠶⣡⢛⡼⣛⠶⡹⢶⡙⣎⠷⣹⣝⣳⢯⣟⣯⢷⣻⢯⣟⡿⣽⣞⣯⢷⡻⢮⡹⡹⣌⠯⣕⡫⢖⣱⢱⡘⠤⠋⣄⢢⢡⠊⡔⣈⠱⡌⢃⠜
⡟⡴⣋⠶⣙⡎⢲⣭⢿⣽⢻⡼⣣⠙⠢⣅⠣⣜⠰⣉⠖⡡⢉⠖⡥⢋⡓⢧⡙⢦⡙⠰⢪⠕⣏⠾⣜⠯⣝⢯⡞⣽⢣⠟⣬⣳⣟⣿⣳⢷⡩⢞⡱⣭⢋⡖⠣⡌⣥⢫⡔⣣⢎⡱⠒⡄⢆⠘⡂⠥
⢿⣱⣏⡖⣣⠜⣱⢮⡷⣫⣟⡵⡃⢀⡁⠆⠓⡌⠳⠌⢎⠡⠃⠎⠐⢩⢌⣃⠹⣂⠡⠓⠤⢊⠜⡸⢌⡳⣍⠶⣙⢦⡛⣞⣳⣟⡾⣷⣟⣧⢻⣭⣓⡌⢧⡜⢧⡹⣆⡳⣙⢦⢋⠴⡉⠔⡈⢰⡉⢇
⢯⠷⣞⣳⢬⠸⣭⢷⡻⣵⢺⡝⣁⢾⡸⣝⢶⡲⣕⠶⣎⢷⣫⢞⡽⡬⣤⢌⣣⣘⢤⡥⣆⡖⡤⣥⣀⡑⠨⠚⠥⢎⢵⣫⢷⣯⣿⣟⣾⡭⢷⣞⣧⡻⣥⢛⢧⣛⡴⢣⡍⢦⡉⢆⢁⣂⢠⢃⡜⢤
⣳⢻⡜⣣⢎⡱⣏⢾⡳⣭⢷⡚⢬⡷⣻⡼⣧⣟⢮⣟⢮⣳⣝⡮⢷⡽⢮⣟⡶⣭⢷⣻⡼⣝⣳⡵⣫⢽⡻⢶⣞⣤⠸⣝⣯⡷⣿⣻⢾⣽⣻⡾⣽⣳⢎⡝⡎⠶⣩⠇⣜⡰⡜⣸⠲⡌⣆⠣⡜⢢
⡷⣋⠾⣥⢫⡴⢫⢧⡛⣭⠾⡽⣣⠻⣵⢻⡵⣞⣟⡾⣏⡷⡾⣽⣛⣾⢻⣼⢻⣭⢷⣳⢟⣭⢷⡻⣝⣾⡹⢷⣎⢯⣽⣻⣞⣿⣳⢯⣟⡾⣣⣟⣷⡻⢎⡰⣉⠳⣤⢛⠴⡣⣕⢣⠣⡝⢤⠓⣍⠆
⣭⠳⣙⠶⡩⢎⠅⡻⣜⢦⣋⠷⣩⠗⣎⢯⣽⢫⡾⡽⣽⣹⢟⣵⡻⢮⡟⣾⣹⢮⡟⣽⠾⣭⡟⣽⣫⢶⡛⢯⡴⣻⣞⡷⣯⢷⢫⢟⣼⣳⢓⡾⡱⡝⣢⠕⣎⡳⢬⣋⢞⡱⡜⢪⡕⡜⢢⡙⢤⡉
⢦⡙⡜⢢⠓⣌⢚⡱⣌⠳⣎⠳⣍⡛⣜⡘⡮⢏⡷⣻⢵⣫⢟⡼⣹⠯⣝⢶⢫⡗⣯⠝⢯⠳⡝⢣⢍⠲⣙⠮⡵⣛⡼⡹⢎⢇⡫⡞⡵⣃⠷⣜⢣⡝⢦⡛⡴⡙⢦⡉⢦⠓⣌⠣⠜⡨⢅⡘⠤⣈
⢆⠰⢈⠆⠱⣀⠣⢒⠌⡓⡌⠳⢄⠹⢤⠓⡘⢭⠲⡱⢎⠶⣩⠚⡥⢛⡌⢎⡱⠚⣄⢋⠆⡣⢉⠖⡨⠣⢍⠚⠥⢓⠬⡑⢎⠲⠱⣙⠲⣉⠞⣌⠣⢎⡱⢜⠲⣉⠦⡙⢦⡙⠤⢓⡘⣐⠢⢌⡐⢀
⢀⠂⠡⢈⠐⠠⠑⣈⠢⠑⡈⢁⠊⡐⠂⠉⠜⡀⠣⠑⠊⠆⡅⢋⠔⠡⠘⠠⠂⠑⠀⡈⠀⠁⡀⢀⠀⢁⠀⠌⠠⢁⠂⠡⢈⠰⠁⢂⠡⠐⠈⠄⡉⢂⠱⢈⠱⢈⠒⠩⡐⠌⡑⠌⠰⢀⠃⠂⠄⢃
⢀⠈⠄⠂⠈⠄⠡⠀⠄⠁⡀⠂⠀⠀⠀⠀⠀⠀⠁⠈⡐⠠⠀⠂⠐⠠⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠂⠐⠠⠈⠐⢀⠂⠐⠈⡐⠀⡈⠐⠈⠀`;
//====================================================================================================
unsafeWindow.Vue = Vue;
const { ref, watch, reactive, createApp } = Vue;
//====================================================================================================
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_;
}
}
//====================================================================================================
function expandMoreVideoPage() {
const getExpandButton = () => document.querySelector('button[data-role="show-more-next"]');
const observer = new Observer((target) => {
target.click();
});
observer.observe(getExpandButton());
}
//====================================================================================================
function createPreviewVideoElement(src, mount) {
const video = document.createElement('video');
video.playsinline = true;
video.webkitplaysinline = true;
video.autoplay = true;
video.loop = true;
video.classList.add('thumb-image-container__video');
video.src = src;
video.addEventListener('loadeddata', () => {
mount.before(video);
}, false);
return {
video,
removeVideo: () => {
video.removeAttribute('src');
video.load();
video.remove();
}
};
}
function handleThumbHover(e) {
if (!e.target.classList.contains('thumb-image-container__image')) return;
const videoSrc = e.target.parentElement.getAttribute('data-previewvideo');
const { video, removeVideo } = createPreviewVideoElement(videoSrc, e.target);
e.target.parentElement.parentElement.addEventListener('mouseleave', removeVideo, { once: true });
}
function animate() {
document.body.addEventListener('mouseover', handleThumbHover);
}
animate();
//====================================================================================================
class XHAMSTER_RULES {
constructor() {
this.IS_VIDEO_PAGE = /^\/videos\//.test(window.location.pathname);
this.PAGINATION = document.querySelector('.prev-next-list') ||
document.querySelector('.test-pager');
this.PAGINATION_LAST = parseInt(document.querySelector('.page-limit-button')?.innerText ||
Array.from(document.querySelectorAll('.page-button-link')).pop()?.innerText ||
document.querySelector('.test-pager')?.lastElementChild.previousElementSibling.innerText);
this.CONTAINER = Array.from(document.querySelectorAll('.thumb-list')).pop();
}
GET_THUMBS(html) {
return html.querySelectorAll('.thumb-list__item:not([data-video-type])');
}
THUMB_IMG_DATA(thumb) {
const img = thumb.querySelector('img[loading]');
if (img) img.removeAttribute('loading');
return img;
}
THUMB_DATA(thumb) {
const title = thumb.querySelector('.video-thumb-info__name').innerText.toLowerCase();
const duration = timeToSeconds(thumb.querySelector('.thumb-image-container__duration').innerText);
return {
title,
duration,
};
}
URL_DATA() {
const { origin, pathname, href } = window.location;
let offset = 1;
let iteratable_url = () => "";
if (/^\/search\//.test(pathname)) {
const url = new URL(href);
offset = url.searchParams.get('page') || 1;
iteratable_url = n => {
url.searchParams.set('page', n);
return url.href;
};
} else {
const mres = location.pathname.split(/\/(\d+)$/);
let base = mres[0];
if (mres[1]) offset = parseInt(mres[1]);
iteratable_url = n => `${origin}${base}/${n}`;
}
return {
offset,
iteratable_url
}
}
}
const RULES = new XHAMSTER_RULES();
//====================================================================================================
class LazyImgLoader {
attributeName = 'data-lazy-load';
constructor(callback) {
this.lazyImgObserver = new Observer((target) => {
callback(target, this.delazify);
});
}
lazify(target, img) {
img.setAttribute(this.attributeName, img.src);
img.src = '';
this.lazyImgObserver.observe(img);
}
delazify = (target) => {
this.lazyImgObserver.observer.unobserve(target);
target.src = target.getAttribute(this.attributeName);
target.removeAttribute(this.attributeName);
}
static create(callback) {
const lazyImgLoader = new LazyImgLoader((target, delazify) => {
if (callback(target)) {
delazify(target);
}
});
return lazyImgLoader;
}
}
//====================================================================================================
class DataManager {
constructor(rules) {
this.rules = rules;
this.data = new Map();
this.lazyImgLoader = LazyImgLoader.create((target) => !this.isFiltered(target));
this.addFilterStyles();
}
dataFilters = {
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);
}
handleLoadedHTML = (html) => {
const thumbs = this.rules.GET_THUMBS(html);
const data_offset = this.data.size;
for (const thumbElement of thumbs) {
const url = thumbElement.firstElementChild.href;
if (!url || this.data.has(url)) continue;
const { title, duration } = this.rules.THUMB_DATA(thumbElement);
this.data.set(url, { element: thumbElement, duration, title });
const img = this.rules.THUMB_IMG_DATA(thumbElement);
if (img) this.lazyImgLoader.lazify(thumbElement, img);
this.rules.CONTAINER.appendChild(thumbElement);
}
this.filterAll(data_offset);
};
}
const { filter_, handleLoadedHTML } = new DataManager(RULES);
//====================================================================================================
const SCROLL_RESET_DELAY = 350;
class PaginationManager {
constructor() {
stateLocale.pagIndexLast = RULES.PAGINATION_LAST;
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);
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 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,
filterNegativeTags: "",
filterNegative: false,
filterPositiveTags: "",
filterPositive: false,
infiniteScrollEnabled: true,
uiEnabled: 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.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 });
//====================================================================================================
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 flex font-mono">exclude</label>
<input type="text" v-model="state.filterNegativeTags" placeholder="tag1, tag2,.." class="w-full h-8 text-zinc-300 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 bg-gray-700">
<label for="include" class="text-zinc-300 flex font-mono">include</label>
<input type="text" v-model="state.filterPositiveTags" placeholder="tag1, tag2,.." class="w-full h-8 text-zinc-300 px-3 py-2 bg-zinc-700 mx-2 rounded-sm size-auto">
</div>
<div class="flex items-center bg-zinc-900 py-2 px-3 m-1 font-mono">
<input type="checkbox" id="infiniteScrollEnabled" v-model="state.infiniteScrollEnabled" class="mr-2 size-auto bg-gray-700">
<label for="infiniteScrollEnabled" class="text-zinc-300 flex 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 }),
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}`);
}
}
//====================================================================================================
console.clear();
console.log(LOGO);
if (RULES.IS_VIDEO_PAGE) {
expandMoreVideoPage();
}
if (RULES.PAGINATION) {
if (!RULES.IS_VIDEO_PAGE) {
const html = parseDOM(RULES.CONTAINER.innerHTML);
RULES.CONTAINER.innerHTML = "";
handleLoadedHTML(html);
}
const paginationManager = new PaginationManager();
}
const ui = new VueApp(state, stateLocale);