您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
infinite scroll, filter: public, private, duration
当前为
// ==UserScript== // @name thisvid infinite scroll and filter // @license MIT // @namespace http://tampermonkey.net/ // @version 2.0.3 // @description infinite scroll, filter: public, private, duration // @author smartacephale // @match https://thisvid.com/ // @match https://thisvid.com/latest-updates/* // @match https://thisvid.com/tags/* // @match https://thisvid.com/categories/* // @match https://thisvid.com/*/?q=* // @match https://thisvid.com/members/* // @match https://thisvid.com/videos/* // @icon https://i.imgur.com/LAhknzo.jpeg // @grant GM_addStyle // @run-at document-end // ==/UserScript== (function () { // biome-ignore lint/suspicious/noRedundantUseStrict: <explanation> 'use strict'; console.clear(); console.log('\n\ntapermonkey-thisvidscript\n\n'); // biome-ignore lint/complexity/noStaticOnlyClass: <explanation> class Utils { static $ = (x, e = document.body) => e.querySelector(x); static $$ = (x, e = document.body) => e.querySelectorAll(x); static findElementsBetweenIds = (startId, endId) => { const startElement = document.getElementById(startId); const endElement = document.getElementById(endId); if (!startElement || !endElement) { return []; } const elementsBetween = []; let currentElement = startElement.nextElementSibling; while (currentElement && currentElement !== endElement) { elementsBetween.push(currentElement); currentElement = currentElement.nextElementSibling; } return elementsBetween; }; static parseHTML = (s) => { const t = document.createElement('html'); t.innerHTML = s; return t; }; static fetchHtml = (url) => fetch(url).then((r) => r.text()).then((h) => parseHTML(h)); static getRandomRgb = () => { const n = Math.round(0xffffff * Math.random()); return `rgb(${n >> 16},${(n >> 8) & 255},${n & 255})`; }; static parseCSSUrl = (s) => s.match(/(?<=\(").*(?="\))/)[0]; static timeToSeconds = (t) => (t.match(/\d+/gm) || ['0']) .reverse() .map((s, i) => parseInt(s) * 60 ** i) .reduce((a, b) => a + b) || 0; static circularShift = (n, c = 6, s = 1) => (n + s) % c || c; static isElementInViewport = (el) => { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }; static toggleClass = (element, className, condition) => { if (condition) { element.classList.add(className); } else { element.classList.remove(className); } }; static parseDOM = (html) => new DOMParser().parseFromString(html, 'text/html').body.firstChild; static parseIntegerOr = (n, or) => Number.isInteger(parseInt(n)) ? parseInt(n) : or; } const { $, $$, findElementsBetweenIds, fetchHtml, parseHTML, parseDOM, parseCSSUrl, circularShift, isElementInViewport, timeToSeconds, getRandomRgb, toggleClass, parseIntegerOr, } = Utils; class Tick { constructor(interval) { this.interval = interval; } start(callback) { if (this.ticker) { this.stop(); } callback(); this.ticker = setInterval(callback, this.interval); } stop() { clearInterval(this.ticker); this.ticker = false; } } class ReactiveLocalStorage { constructor(data) { if (data) { Object.assign(this, data); this.observeProps(this); } } getLS(prop) { return JSON.parse(localStorage.getItem(prop)); } setLS(prop, value) { localStorage.setItem(prop, JSON.stringify(value)); } toObservable(obj, prop) { const lsvalue = this.getLS(prop); let value = lsvalue !== null ? lsvalue : obj[prop]; Object.defineProperty(obj, prop, { get() { return value; }, set(newValue) { this.setLS(prop, newValue); value = newValue; }, }); } observeProps(obj) { for (const [key, _] of Object.entries(obj)) { this.toObservable(obj, key); } } } const SCROLL_RESET_DELAY = 500; const ANIMATION_DELAY = 750; class DomManager { constructor() { this.data = new Map(); this.container = this.createThumbsContainer(); let filterByText = false; this.buffer = ''; // press shift and type text, then press shift again and filter is gone window.addEventListener('keydown', (event) => { if (event.shiftKey) { this.buffer = '' filterByText = !filterByText; if (!filterByText) this.filterByTextF(false); } else if (filterByText && event.key.match(/^[a-zA-Z]$/g)) { this.buffer += event.key; //console.log({'buffer': this.buffer, filterByText}); if (filterByText && this.buffer.length > 0) { this.filterByTextF(true, this.buffer); } } }); } filterByTextF(filter, text=''){ for (const [k,v] of this.data.entries()) { toggleClass(v.element, 'filtered-text', filter && !k.includes(text)); }; } thumbIsPrivate(t) { return t.firstElementChild.classList.contains('private'); } filterPrivate = (filterPrivate = state.filterPrivate) => { for (const v of this.data.values()) { toggleClass(v.element, 'filtered-private', filterPrivate && this.thumbIsPrivate(v.element)) }; } filterPublic = (filterPublic = state.filterPublic) => { for (const v of this.data.values()) { toggleClass(v.element, 'filtered-public', filterPublic && !this.thumbIsPrivate(v.element)) }; } filterByDuration = () => { const { filterDurationFrom: from, filterDurationTo: to, filterDuration } = state; for (const v of this.data.values()) { toggleClass(v.element, 'filtered-duration', filterDuration && (v.duration < from || v.duration > to)) }; } runFilters(container) { if (state.filterPrivate) this.filterPrivate(container); if (state.filterPublic) this.filterPublic(container); if (state.filterDuration) this.filterByDuration(container); } createThumbsContainer() { return parseDOM('<div class="thumbs-items"></div>'); } handleLoadedHTML = (htmlPage, mount, useGlobalContainer = true) => { const thumbs = $$('.tumbpu', htmlPage); const container = !useGlobalContainer ? this.createThumbsContainer() : this.container; for (const thumbElement of thumbs) { const url = thumbElement.getAttribute('href'); if (!url || this.data.has(url)) { thumbElement.remove(); } else { this.data.set(url, { element: thumbElement, duration: timeToSeconds($('.thumb > .duration', thumbElement).textContent) }); const img = $('img', thumbElement); const privateEl = $('.private', thumbElement); if (privateEl) { img.src = parseCSSUrl(privateEl.style.background); privateEl.style.background = '#000'; } else { img.src = img.getAttribute('data-original'); } img.classList.add('tracking'); container.appendChild(thumbElement); } } this.runFilters(container); mount.before(container); }; } class PreviewManager { constructor() { this.tick = new Tick(ANIMATION_DELAY); } iteratePreviewImages(src) { return src.replace(/(\d)\.jpg$/, (_, n) => `${circularShift(parseInt(n))}.jpg`); } animatePreview = (e) => { const { target: el, type } = e; if (el.tagName === 'IMG' && el.classList.contains('tracking')) { if (type === 'mouseout') { this.tick.stop(); if (el.getAttribute('orig')) el.src = el.getAttribute('orig'); } if (type === 'mouseover') { if (!el.getAttribute('orig')) el.setAttribute('orig', el.src); this.tick.start(() => { el.src = this.iteratePreviewImages(el.src); }); } } }; listen(e) { e.addEventListener('mouseout', this.animatePreview); e.addEventListener('mouseover', this.animatePreview); } } class PaginationPageManager { constructor() { this.pagination = $('.pagination'); this.pagination.style.opacity = 0; handleLoadedHTML(document.body, this.pagination); previewManager.listen(this.pagination.parentElement); this.offsetLast = this.getOffsetLast(); this.paginationGenerator = this.createNextPageGenerator(); this.generatorDone = false; this.resetScroller(); this.tick = new Tick(SCROLL_RESET_DELAY); this.fixScrollViewPort(); this.ui = new UI(true); this.setPagIndex = (offset) => this.ui.setPagIndex(offset, this.offsetLast); this.setPagIndex(this.getCurrentOffset()); } fixScrollViewPort() { this.tick.start(() => { if (this.generatorDone) this.tick.stop(); if (isElementInViewport(this.pagination)) this.generatorConsumer(); }); } async generatorConsumer() { const { value: { url, offset } = {}, done, } = this.paginationGenerator.next(); this.generatorDone = done; if (!done) { const nextPageHTML = await fetchHtml(url); const prevScrollPos = document.documentElement.scrollTop; handleLoadedHTML(nextPageHTML, this.pagination); this.setPagIndex(offset); window.scrollTo(0, prevScrollPos); } } getCurrentOffset() { return parseInt(window.location.pathname.split(/(\d+\/)$/)[1] || '1'); } getOffsetLast() { return parseInt( $('.pagination-next').previousElementSibling.firstElementChild .textContent, ); } createNextPageGenerator() { let { origin, pathname, search } = window.location; let offset; [pathname, offset = '1'] = pathname.split(/(\d+\/)$/); offset = parseInt(offset); pathname = pathname === '/' ? '/latest-updates/' : pathname; const offsetLast = this.getOffsetLast(); function* nextPageGenerator() { for (let c = offset + 1; c <= offsetLast; c++) { const url = `${origin}${pathname}${c}/${search}`; console.log(url); yield { url, offset: c }; } } return nextPageGenerator(); } resetScroller = () => { this.infiniteScrollTriggered = false; window.dispatchEvent(new CustomEvent('scroll')); }; infiniteScroll = () => { const inViewport = isElementInViewport(this.pagination); if (inViewport === this.infiniteScrollTriggered) return; this.infiniteScrollTriggered = inViewport; if (inViewport) this.generatorConsumer(); }; } class Router { constructor() { this.route(); } route() { const { pathname } = window.location; if ($('.pagination-next')) { this.handlePaginationPage(); } else if (/\/members\/\d+\/$/.test(pathname)) { this.handleMemberPage(); } else if (/\/tag\//.test(pathname) || /\/?q=.*/.test(pathname)) { this.handlePageWithVideosButNoPagination(); } else if (/\/videos\//.test(pathname)) { //this.handlePageWithVideosButNoPagination() } } handlePageWithVideosButNoPagination() { const vid = $('.tumbpu'); if (!vid) return; handleLoadedHTML(document.body, vid.parentElement); previewManager.listen(vid.parentElement); const ui = new UI(false); } handlePaginationPage() { this.paginationManager = new PaginationPageManager(); } handleMemberPage() { const privates = $('#list_videos_private_videos_items'); if (privates) { const mistakes = findElementsBetweenIds( 'list_videos_private_videos_items', 'list_videos_favourite_videos', ); for (const m of mistakes) privates.appendChild(m); handleLoadedHTML(privates, privates, false); } const favorites = $('#list_videos_favourite_videos'); if (favorites) { const mountTo = favorites.firstElementChild.nextElementSibling; handleLoadedHTML(favorites, mountTo, false); } if (privates || favorites) { previewManager.listen((privates || favorites).parentElement); const ui = new UI(false); } } } class UI { templateHTML = (haspag) => ` <div id="tapermonkey-app"> <div class="subbox"> <input type="checkbox" id="filterPrivate" name="filterPrivate" ${state.filterPrivate ? 'checked' : '' }/> <label for="filterPrivate">filter private</label> <input type="checkbox" id="filterPublic" name="filterPublic" ${state.filterPublic ? 'checked' : '' }/> <label for="filterPublic">filter public</label> ${haspag ? '<span id="pagIndex">0/0</span>' : ''} </div> <div class="subbox"> <input type="checkbox" id="filterl" name="filterl" ${state.filterDuration ? 'checked' : '' } /> <label for="filterl">filter duration seconds</label> <label for="from">from</label> <input type="number" placeholder="min sec" step="10" min="0" max="72000" id="minL" name="from" value=${state.filterDurationFrom} /> <label for="to">to</label> <input type="number" placeholder="max sec" step="10" min="0" max="72000" id="maxL" name="to" value=${state.filterDurationTo} /> </div> </div>`; constructor(haspag = true) { document.body.appendChild(parseDOM(this.templateHTML(haspag))); this.tapermonkeyAppTemplate = document.querySelector('#tapermonkey-app'); this.control(); } setPagIndex(index, total) { $('#pagIndex').innerText = `${index}/${total}`; } control() { this.tapermonkeyAppTemplate.addEventListener('click', (e) => { const { id, checked, value } = e.target; if (id === 'filterPublic') { state.filterPublic = checked; filterPublic(); } if (id === 'filterPrivate') { state.filterPrivate = checked; filterPrivate(); } if (id === 'filterl') { state.filterDuration = checked; filterByDuration(); } if (id === 'minL') { state.filterDurationFrom = parseIntegerOr(value, state.filterDurationFrom); filterByDuration(); } if (id === 'maxL') { state.filterDurationTo = parseIntegerOr(value, state.filterDurationTo); filterByDuration(); } }); } } const state = new ReactiveLocalStorage({ filterDurationFrom: 0, filterDurationTo: 600, filterDuration: false, filterPrivate: false, filterPublic: false, infiniteScrollTriggered: false, }); const { filterPrivate, filterPublic, filterByDuration, handleLoadedHTML } = new DomManager(); const previewManager = new PreviewManager(); const router = new Router(); const tampermonkeyCSS = ` #tapermonkey-app { background: #151515; padding: 10px; position: fixed; z-index: 9999; bottom: 10px; right: 10px; border-radius: 15px; width: max-content; box-shadow: 20px 20px 60px #000000, -20px -20px 60px #000000; } #tapermonkey-app .subbox { background: #2c2c2c; border-radius: 5px; padding: 4px; margin: 6px; user-select: none; display: flex; } #tapermonkey-app .subbox input[type=number] { width: 5rem; background: #26282b; } #tapermonkey-app .subbox input[type=checkbox] { margin-left: 5px; } #tapermonkey-app .subbox label { margin: 0 10px 0 0; } #pagIndex { text-align: end; margin-right: 5px; flex: 1;} #tapermonkey-app .subbox * { padding-left: 8px; float: none; width: auto; font-family: monospace; font-size: 0.8rem; align-self: center; color: #969696; } .tracking { content-visibility: auto; } .filtered-private, .filtered-duration, .filtered-public, .filtered-text { display: none !important; } `; GM_addStyle(tampermonkeyCSS); })();