// ==UserScript== // @name Better Youtube Shorts // @name:zh-CN 更好的 Youtube Shorts // @name:zh-TW 更好的 Youtube Shorts // @namespace Violentmonkey Scripts // @version 1.6.7 // @description Provides more control features for Youtube Shorts, including volume control, progress bar, auto-scroll, hotkeys, and more. // @description:zh-CN 为 Youtube Shorts提供更多的控制功能,包括音量控制,进度条,自动滚动,快捷键等等。 // @description:zh-TW 為 Youtube Shorts提供更多的控制功能,包括音量控制,進度條,自動滾動,快捷鍵等等。 // @author Meriel // @match *://www.youtube.com/* // @run-at document-start // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @downloadURL none // ==/UserScript== (() => { let userscriptInitialized = false; (() => { const urlChange = (event = null) => { const destinationUrl = event?.destination?.url || ""; if (destinationUrl.startsWith("about:blank")) return; const href = destinationUrl || location.href; const isShorts = href.includes("youtube.com/shorts"); if (userscriptInitialized && !isShorts) { localtion.href = destinationUrl; userscriptInitialized = false; } else if (!userscriptInitialized && isShorts) { (() => { let volumeStyle = GM_getValue("volumeStyle"); if (volumeStyle === void 0) { volumeStyle = "speaker"; GM_setValue("volumeStyle", volumeStyle); } GM_addStyle( `input[type="range"].volslider { height: 14px; -webkit-appearance: none; margin: 10px 0; } input[type="range"].volslider:focus { outline: none; } input[type="range"].volslider::-webkit-slider-runnable-track { height: 8px; cursor: pointer; box-shadow: 0px 0px 0px #000000; background: rgb(50 50 50); border-radius: 25px; border: 1px solid #000000; } ${ volumeStyle === "dot" ? `input[type="range"].volslider::-webkit-slider-thumb { -webkit-appearance: none; width: 15px; height: 15px; margin-top: -4px; border-radius: 50%; background: white; }` : `input[type="range"].volslider::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; margin-top: -7px; border-radius: 0px; background-image: url("https://i.imgur.com/vcQoCVS.png"); background-size: 20px; background-repeat: no-repeat; background-position: 50%; }` } } input[type="range"]:focus::-webkit-slider-runnable-track { background: rgb(50 50 50); } .switch { position: relative; display: inline-block; width: 46px; height: 20px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; -webkit-transition: 0.4s; transition: 0.4s; } .slider:before { position: absolute; content: ""; height: 12px; width: 12px; left: 4px; bottom: 4px; background-color: white; -webkit-transition: 0.4s; transition: 0.4s; } input:checked + .slider { background-color: #ff0000; } input:focus + .slider { box-shadow: 0 0 1px #ff0000; } input:checked + .slider:before { -webkit-transform: translateX(26px); -ms-transform: translateX(26px); transform: translateX(26px); } /* Rounded sliders */ .slider.round { border-radius: 12px; } .slider.round:before { border-radius: 50%; }` ); let seekMouseDown = false; let lastCurSeconds = 0; let video = null; let audioInitialized = false; let autoScroll = GM_getValue("autoScroll", true); let volume = GM_getValue("volume", 0); let constantVolume = GM_getValue("constantVolume"); let operationMode = GM_getValue("operationMode"); if (constantVolume === void 0) { constantVolume = false; GM_setValue("constantVolume", constantVolume); } if (operationMode === void 0) { operationMode = "Shorts"; GM_setValue("operationMode", operationMode); } GM_registerMenuCommand( `Constant Volume: ${constantVolume ? "On" : "Off"}`, function () { constantVolume = !constantVolume; GM_setValue("constantVolume", constantVolume); location.reload(); } ); GM_registerMenuCommand( `Operating mode: ${operationMode}`, function () { operationMode = operationMode === "Video" ? "Shorts" : "Video"; GM_setValue("operationMode", operationMode); location.reload(); } ); GM_registerMenuCommand(`Volume Style: ${volumeStyle}`, () => { volumeStyle = volumeStyle === "speaker" ? "dot" : "speaker"; GM_setValue("volumeStyle", volumeStyle); location.reload(); }); const observer = new MutationObserver( (mutations, shortsReady = false, videoPlayerReady = false) => { outer: for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!shortsReady) { shortsReady = node.tagName === "YTD-SHORTS"; } if (!videoPlayerReady) { videoPlayerReady = typeof node.className === "string" && node.className.includes("html5-main-video"); } if (shortsReady && videoPlayerReady) { observer.disconnect(); video = node; if (constantVolume) { video.volume = volume; } addShortcuts(); updateVidElemWithRAF(); break outer; } } } } ); observer.observe(document.documentElement, { childList: true, subtree: true, }); function videoOperationMode(e) { if (!e.shiftKey) { if ( e.key.toUpperCase() === "ARROWUP" || e.key.toUpperCase() === "ARROWDOWN" ) { e.stopPropagation(); e.preventDefault(); const volumeSlider = document.querySelector("#byts-vol"); switch (e.key.toUpperCase()) { case "ARROWUP": video.volume = Math.min(1, video.volume + 0.01); volumeSlider.value = video.volume; break; case "ARROWDOWN": video.volume = Math.max(0, video.volume - 0.01); volumeSlider.value = video.volume; break; default: break; } } else if ( e.key.toUpperCase() === "ARROWLEFT" || e.key.toUpperCase() === "ARROWRIGHT" ) { switch (e.key.toUpperCase()) { case "ARROWLEFT": video.currentTime -= 1; break; case "ARROWRIGHT": video.currentTime += 1; break; default: break; } } } else { switch (e.key.toUpperCase()) { case "ARROWLEFT": case "ARROWUP": navigationButtonUp(); break; case "ARROWRIGHT": case "ARROWDOWN": navigationButtonDown(); break; default: break; } } } function shortsOperationMode(e) { if ( e.key.toUpperCase() === "ARROWUP" || e.key.toUpperCase() === "ARROWDOWN" ) { e.stopPropagation(); e.preventDefault(); const volumeSlider = document.querySelector("#byts-vol"); if (e.shiftKey) { switch (e.key.toUpperCase()) { case "ARROWUP": video.volume = Math.min(1, video.volume + 0.02); volumeSlider.value = video.volume; break; case "ARROWDOWN": video.volume = Math.max(0, video.volume - 0.02); volumeSlider.value = video.volume; break; default: break; } } else { switch (e.key.toUpperCase()) { case "ARROWUP": navigationButtonUp(); break; case "ARROWDOWN": navigationButtonDown(); break; default: break; } } } else if ( e.key.toUpperCase() === "ARROWLEFT" || e.key.toUpperCase() === "ARROWRIGHT" ) { const volumeSlider = document.querySelector("#byts-vol"); if (e.shiftKey) { switch (e.key.toUpperCase()) { case "ARROWLEFT": video.volume = Math.max(0, video.volume - 0.01); volumeSlider.value = video.volume; break; case "ARROWRIGHT": video.volume = Math.min(1, video.volume + 0.01); volumeSlider.value = video.volume; break; default: break; } } else { switch (e.key.toUpperCase()) { case "ARROWLEFT": video.currentTime -= 1; break; case "ARROWRIGHT": video.currentTime += 1; break; default: break; } } } } function addShortcuts() { if (operationMode === "Video") { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node?.id === "byts-vol-div") { document.addEventListener( "keydown", (e) => { videoOperationMode(e); if (constantVolume) { constantVolume = false; requestAnimationFrame( () => (constantVolume = true) ); } }, { capture: true, } ); observer.disconnect(); } } } }); observer.observe(document.documentElement, { childList: true, subtree: true, }); } else { document.addEventListener( "keydown", (e) => { shortsOperationMode(e); if (constantVolume) { constantVolume = false; requestAnimationFrame(() => (constantVolume = true)); } }, { capture: true, } ); } video.addEventListener("dblclick", () => { if (document.fullscreenElement) { document.exitFullscreen(); } else { document.querySelector("ytd-app").requestFullscreen(); } }); document.addEventListener("keydown", (e) => { if ( e.key.toUpperCase() === "ENTER" || e.key.toUpperCase() === "NUMPADENTER" ) { if (document.fullscreenElement) { document.exitFullscreen(); } else { document.querySelector("ytd-app").requestFullscreen(); } } }); } function padTo2Digits(num) { return num.toString().padStart(2, "0"); } function updateVidElemWithRAF() { try { updateVidElem(); } catch (_) {} requestAnimationFrame(updateVidElemWithRAF); } function navigationButtonDown() { document.querySelector("#navigation-button-down button").click(); } function navigationButtonUp() { document.querySelector("#navigation-button-up button").click(); } function setVideoPlaybackTime(event, player) { let rect = player.getBoundingClientRect(); let offsetX = event.clientX - rect.left; if (offsetX < 0) { offsetX = 0; } else if (offsetX > player.offsetWidth) { offsetX = player.offsetWidth - 1; } video.currentTime = (offsetX / player.offsetWidth) * video.duration; } function updateVidElem() { const currentVideo = document.querySelector( "#shorts-player > div.html5-video-container > video" ); if (video !== currentVideo) { video = currentVideo; } if (!audioInitialized && constantVolume) { video.volume = volume; } const reel = document.querySelector( "ytd-reel-video-renderer[is-active]" ); if (reel === null) { return; } if (operationMode === "Shorts") { document.removeEventListener("keydown", videoOperationMode, { capture: true, }); document.addEventListener("keydown", shortsOperationMode, {}); } else { document.removeEventListener("keydown", shortsOperationMode, {}); document.addEventListener("keydown", videoOperationMode, { capture: true, }); } // Volume Slider let volumeSliderDiv = reel.querySelector("#byts-vol-div"); let volumeSlider = reel.querySelector("#byts-vol"); let volumeTextDiv = reel.querySelector("#byts-vol-textdiv"); if (volumeSliderDiv === null) { volumeSliderDiv = document.createElement("div"); volumeSliderDiv.id = "byts-vol-div"; volumeSliderDiv.style.cssText = `user-select: none; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-left: 5px; margin-top: ${ reel.offsetHeight + 5 }px;`; volumeSlider = document.createElement("input"); volumeSlider.style.cssText = `user-select: none; width: 80px; left: 0px; background-color: transparent; position: absolute; margin-top: 0px;`; volumeSlider.type = "range"; volumeSlider.id = "byts-vol"; volumeSlider.className = "volslider"; volumeSlider.name = "vol"; volumeSlider.min = 0.0; volumeSlider.max = 1.0; volumeSlider.step = 0.01; volumeSlider.value = video.volume; volumeSlider.addEventListener("input", function () { video.volume = this.value; GM_setValue("volume", this.value); }); volumeSliderDiv.appendChild(volumeSlider); volumeTextDiv = document.createElement("div"); volumeTextDiv.id = "byts-vol-textdiv"; volumeTextDiv.style.cssText = `user-select: none; background-color: transparent; position: absolute; color: white; font-size: 1.2rem; margin-left: ${ volumeSlider.offsetWidth + 5 }px`; volumeTextDiv.textContent = `${( video.volume.toFixed(2) * 100 ).toFixed()}%`; volumeSliderDiv.appendChild(volumeTextDiv); reel.appendChild(volumeSliderDiv); audioInitialized = true; } if (constantVolume) { video.volume = volumeSlider.value; } volumeSlider.value = video.volume; volumeTextDiv.textContent = `${( video.volume.toFixed(2) * 100 ).toFixed()}%`; volumeSliderDiv.style.marginTop = `${reel.offsetHeight + 5}px`; volumeTextDiv.style.marginLeft = `${ volumeSlider.offsetWidth + 5 }px`; // Progress Bar let progressBar = reel.querySelector("#byts-progbar"); if (progressBar === null) { const builtinProgressbar = reel.querySelector("#progress-bar"); if (builtinProgressbar !== null) { builtinProgressbar.remove(); } if (progressBar === null) { progressBar = document.createElement("div"); progressBar.id = "byts-progbar"; progressBar.style.cssText = `user-select: none; cursor: pointer; width: 98%; height: 6px; background-color: #343434; position: absolute; border-radius: 10px; margin-top: ${ reel.offsetHeight - 6 }px;`; } reel.appendChild(progressBar); let wasPausedBeforeDrag = false; progressBar.addEventListener("mousedown", (e) => { seekMouseDown = true; wasPausedBeforeDrag = video.paused; setVideoPlaybackTime(e, progressBar); video.pause(); }); document.addEventListener("mousemove", (e) => { if (!seekMouseDown) return; setVideoPlaybackTime(e, progressBar); if (!video.paused) { video.pause(); } e.preventDefault(); }); document.addEventListener("mouseup", () => { if (!seekMouseDown) return; seekMouseDown = false; if (!wasPausedBeforeDrag) { video.play(); } }); } progressBar.style.marginTop = `${reel.offsetHeight - 6}px`; // Progress Bar (Inner Red Bar) const progressTime = (video.currentTime / video.duration) * 100; let InnerProgressBar = progressBar.querySelector("#byts-progress"); if (InnerProgressBar === null) { InnerProgressBar = document.createElement("div"); InnerProgressBar.id = "byts-progress"; InnerProgressBar.style.cssText = `user-select: none; background-color: #FF0000; height: 100%; border-radius: 10px; width: ${progressTime}%;`; progressBar.appendChild(InnerProgressBar); } InnerProgressBar.style.width = `${progressTime}%`; // Time Info const durSecs = Math.floor(video.duration); const durMinutes = Math.floor(durSecs / 60); const durSeconds = durSecs % 60; const curSecs = Math.floor(video.currentTime); let timeInfo = reel.querySelector("#byts-timeinfo"); let timeInfoText = reel.querySelector("#byts-timeinfo-textdiv"); if ( !Number.isNaN(durSecs) && reel.querySelector("#byts-timeinfo") !== null ) { timeInfoText.textContent = `${Math.floor( curSecs / 60 )}:${padTo2Digits(curSecs % 60)} / ${durMinutes}:${padTo2Digits( durSeconds )}`; } if ( curSecs !== lastCurSeconds || reel.querySelector("#byts-timeinfo") === null ) { lastCurSeconds = curSecs; const curMinutes = Math.floor(curSecs / 60); const curSeconds = curSecs % 60; if (timeInfo === null) { timeInfo = document.createElement("div"); timeInfo.id = "byts-timeinfo"; timeInfo.style.cssText = `user-select: none; display: flex; right: auto; left: auto; position: absolute; margin-top: ${ reel.offsetHeight + 2 }px;`; timeInfoText = document.createElement("div"); timeInfoText.id = "byts-timeinfo-textdiv"; timeInfoText.style.cssText = `display: flex; margin-right: 5px; margin-top: 4px; color: white; font-size: 1.2rem;`; timeInfoText.textContent = `${curMinutes}:${padTo2Digits( curSeconds )} / ${durMinutes}:${padTo2Digits(durSeconds)}`; timeInfo.appendChild(timeInfoText); reel.appendChild(timeInfo); timeInfoText.textContent = `${curMinutes}:${padTo2Digits( curSeconds )} / ${durMinutes}:${padTo2Digits(durSeconds)}`; } } timeInfo.style.marginTop = `${reel.offsetHeight + 2}px`; // AutoScroll let autoScrollDiv = reel.querySelector("#byts-autoscroll-div"); if (autoScrollDiv === null) { autoScrollDiv = document.createElement("div"); autoScrollDiv.id = "byts-autoscroll-div"; autoScrollDiv.style.cssText = `user-select: none; display: flex; right: 0px; position: absolute; margin-top: ${ reel.offsetHeight + 2 }px;`; const autoScrollTextDiv = document.createElement("div"); autoScrollTextDiv.style.cssText = `display: flex; margin-right: 5px; margin-top: 4px; color: white; font-size: 1.2rem;`; autoScrollTextDiv.textContent = "Auto Scroll "; autoScrollDiv.appendChild(autoScrollTextDiv); const autoScrollSwitch = document.createElement("label"); autoScrollSwitch.className = "switch"; const autoscrollInput = document.createElement("input"); autoscrollInput.id = "byts-autoscroll-input"; autoscrollInput.type = "checkbox"; autoscrollInput.checked = autoScroll; autoscrollInput.addEventListener("input", function () { autoScroll = this.checked; GM_setValue("autoScroll", this.checked); }); const autoScrollSlider = document.createElement("span"); autoScrollSlider.className = "slider round"; autoScrollSwitch.appendChild(autoscrollInput); autoScrollSwitch.appendChild(autoScrollSlider); autoScrollDiv.appendChild(autoScrollSwitch); reel.appendChild(autoScrollDiv); } if (autoScroll === true) { video.removeAttribute("loop"); video.removeEventListener("ended", navigationButtonDown); video.addEventListener("ended", navigationButtonDown); } else { video.setAttribute("loop", true); video.removeEventListener("ended", navigationButtonDown); } autoScrollDiv.style.marginTop = `${reel.offsetHeight + 2}px`; } })(); userscriptInitialized = true; } }; const historyWrap = (type) => { const origin = unsafeWindow.history[type]; const event = new Event(type); return () => { const rv = origin.apply(this, arguments); event.arguments = arguments; unsafeWindow.dispatchEvent(event); return rv; }; }; urlChange(); if (unsafeWindow.navigation) { unsafeWindow.navigation.addEventListener("navigate", (event) => { urlChange(event); }); return; } unsafeWindow.history.pushState = historyWrap("pushState"); unsafeWindow.history.replaceState = historyWrap("replaceState"); unsafeWindow.addEventListener("replaceState", (event) => { urlChange(event); }); unsafeWindow.addEventListener("pushState", (event) => { urlChange(event); }); unsafeWindow.addEventListener("popstate", (event) => { urlChange(event); }); unsafeWindow.addEventListener("hashchange", (event) => { urlChange(event); }); })(); })();