您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
allows you to set different volumes for different twitch channels!
// ==UserScript== // @name volume set per channel // @namespace http://tampermonkey.net/ // @version 2025-08-02 // @description allows you to set different volumes for different twitch channels! // @author trevrosa // @run-at document-body // @match https://www.twitch.tv/* // @icon https://www.google.com/s2/favicons?sz=64&domain=twitch.tv // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== function log(msg) { console.log(`volumeset: ${msg}`) } // https://stackoverflow.com/a/61511955 function waitForElem(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336 observer.observe(document.body, { childList: true, subtree: true }); }); } (function() { 'use strict'; async function getChannelName() { const page = window.location.pathname; // https://twitch.tv(/xdd) // ^ one `/`, two split regions if (page.split("/").length == 2) { return page; } else { log("in a vod: waiting for channel name"); const name = (await waitForElem("h1[class*='ScTitleText']")).innerText; return `/${name.toLowerCase()}`; } } async function setVolume(slider) { if (!slider) { log("could not set volume: slider was null, retrying"); setTimeout(() => { setVolume(document.querySelector("input[type='range']")) }, 1000); return; } const channel = await getChannelName(); const volume = GM_getValue(channel, null); if (!volume) { log(`no saved volume for channel '${channel}'`); return; } // change the slider's value to what we want slider.value = volume; // invoke the react event handler to then change the volume (https://stackoverflow.com/a/77083516) const reactHandlerKey = Object.keys(slider).find(key => key.startsWith('__reactProps$')); // changed to reactProps const changeEvent = new Event('change', { bubbles: true }); Object.defineProperty(changeEvent, 'currentTarget', {writable: false, value: slider}); // https://stackoverflow.com/a/49122553 slider[reactHandlerKey].onChange(changeEvent); log(`set the volume to ${volume} (${channel})`) } let listenerSet = false; function setListener(slider) { if (!slider) { log("could not set listener: slider was null"); return; } slider.onchange = async (e) => { const volume = parseFloat(e.target.value); const channel = await getChannelName(); GM_setValue(channel, volume) log(`${channel} saved to ${volume} volume`); } log("set volume slider listener"); listenerSet = true; } // skip the main page, if we switch to an actual channel, we set the listener then. if (window.location.pathname != "/") { log("waiting for volume slider") waitForElem("input[type='range']").then(async (slider) => { await setListener(slider); await setVolume(slider); }); } let lastPage = window.location.pathname; setInterval(async () => { const curPage = window.location.pathname; // ignore main page if (curPage == "/") return; if (!listenerSet) { const slider = document.querySelector("input[type='range']"); setListener(slider); setVolume(slider); } if (curPage != lastPage) { lastPage = curPage; await setVolume(document.querySelector("input[type='range']")); } }, 500); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址