您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto-set sort to "opt1" (Viewers High->Low) with configurable run policy
// ==UserScript== // @name Twitch - Force sort Viewers High to Low // @namespace https://twitch.tv/ // @version 1.6 // @description Auto-set sort to "opt1" (Viewers High->Low) with configurable run policy // @author Vikindor // @license MIT // @match https://www.twitch.tv/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ---------------- CONFIG ---------------- // RUN_POLICY options: // - 'perTab' : run once per URL per tab session (F5 won't run again) // - 'perLoad' : run once per URL per page load (F5 will run again) const RUN_POLICY = 'perTab'; // If necessary, change this option and save changes // Substring that appears in sort dropdown ids/controls const SORT_ID_SUBSTR = 'browse-sort-drop-down'; // The target option id suffix: "opt1" = Viewers (High to Low) in current Twitch UI const TARGET_SUFFIX = 'opt1'; // --------------------------------------- // utils const waitFor = (selector, { timeout = 15000, interval = 150, filter = null } = {}) => new Promise((resolve, reject) => { const t0 = Date.now(); (function poll() { const nodes = Array.from(document.querySelectorAll(selector)); const el = filter ? nodes.find(filter) : nodes[0]; if (el) return resolve(el); if (Date.now() - t0 > timeout) return reject(new Error('timeout:' + selector)); setTimeout(poll, interval); })(); }); const safeClick = (el) => { try { el.click(); } catch (_) {} }; // ---------------- Defocus helpers ---------------- // Kills focus on Twitch headings that grab focus on page open (Browse h1, channel title h1, etc.) const HEADING_FOCUS_SEL = [ 'h1.tw-title', 'h1[tabindex="-1"]', '[role="heading"].tw-title', '[data-test-selector="channel-header-title"] h1', ].join(','); function defocusWeirdHeading() { const el = document.activeElement; if (!el || el === document.body) return; // Only defocus known heading-like elements if ( el.matches(HEADING_FOCUS_SEL) || ((el.getAttribute('role') === 'heading' || /^H\d$/.test(el.tagName)) && el.tabIndex === -1) ) { try { el.blur(); } catch (_) {} } } // So outline doesn't flash even for a tick (function injectNoOutlineCSS() { const css = ` ${HEADING_FOCUS_SEL}:focus { outline: none !important; box-shadow: none !important; } `; const style = document.createElement('style'); style.textContent = css; document.documentElement.appendChild(style); })(); // keying const urlPart = () => { const u = new URL(location.href); u.searchParams.delete('sort'); // ignore Twitch sort param return `${u.pathname}${u.search}`; }; const loadPart = () => `${performance.timeOrigin}`; // unique per page load const keyForUrl = () => { if (RUN_POLICY === 'perLoad') return `tw_sort_opt1_${urlPart()}_${loadPart()}`; if (RUN_POLICY === 'perTab') return `tw_sort_opt1_${urlPart()}`; return ''; }; const alreadyRan = () => !!sessionStorage.getItem(keyForUrl()); const markRan = () => sessionStorage.setItem(keyForUrl(), '1'); async function ensureSortOpt1() { // If no sort combobox on this page (e.g., channel page), still remove accidental focus if (!document.querySelector(`[role="combobox"][aria-controls*="${SORT_ID_SUBSTR}"]`)) { defocusWeirdHeading(); return; } if (alreadyRan()) return; try { const combo = await waitFor( `[role="combobox"][aria-controls*="${SORT_ID_SUBSTR}"]` ); const current = combo.getAttribute('aria-activedescendant') || ''; if (current.endsWith(TARGET_SUFFIX)) { defocusWeirdHeading(); markRan(); return; } safeClick(combo); const option = await waitFor( `[id$="${TARGET_SUFFIX}"][role="menuitemradio"], [id$="${TARGET_SUFFIX}"][role="option"], [id$="${TARGET_SUFFIX}"]`, { filter: (el) => !!(el.offsetParent || el.getClientRects().length) } ); safeClick(option); // Remove focus that sometimes lands on page headings setTimeout(defocusWeirdHeading, 0); markRan(); } catch (_) { // silent fail setTimeout(defocusWeirdHeading, 0); } } // initial run setTimeout(() => { defocusWeirdHeading(); ensureSortOpt1(); }, 500); // Also catch any later unexpected focus (e.g., Twitch scripts refocus) window.addEventListener('focusin', defocusWeirdHeading, true); // SPA navigation hook (function hookHistory() { const fire = () => window.dispatchEvent(new Event('locationchange')); const p = history.pushState, r = history.replaceState; history.pushState = function () { p.apply(this, arguments); fire(); }; history.replaceState = function () { r.apply(this, arguments); fire(); }; window.addEventListener('popstate', fire); })(); window.addEventListener('locationchange', () => { setTimeout(() => { defocusWeirdHeading(); ensureSortOpt1(); }, 600); }); })();