// ==UserScript== // @name Brtter Youtube Shorts // @name:zh-CN 更好的Youtube Shorts // @name:zh-TW 更好的Youtube Shorts // @name:ja より良いYoutube Shorts // @namespace Violentmonkey Scripts // @version 1.2.1 // @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提供更多的控制功能,包括音量控制,進度條,自動滾動,快捷鍵等等。 // @description:ja Youtube Shortsに音量コントロール、プログレスバー、自動スクロール、ホットキーなどの機能を提供します。 // @author Meriel // @match *://www.youtube.com/shorts/* // @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== 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; } 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%; }` ); var vid = null; var reel = null; var progbar = null; var seekMouseDown = false; var bytsVol = null; var bytsTimeInfo = null; var lastCurSeconds = 0; var progress = null; var progressTime = 0; var shortsAndPlayerReady = 0; var autoScrollVal = true; var autoscrollInput = null; var constantVolume = false; GM_registerMenuCommand(constantVolume ? "Constant Volume: On" : "Constant Volume: Off", function () { constantVolume = !constantVolume; GM_setValue("bytsConstantVolume", constantVolume); }); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { try { if ( node.tagName === "YTD-SHORTS" || node.classList.contains("html5-video-player") ) { shortsAndPlayerReady++; if (shortsAndPlayerReady === 2) { observer.disconnect(); autoScrollVal = GM_getValue("bytsAutoscroll", true); constantVolume = GM_getValue("bytsConstantVolume", false); updateVidElemWithRAF(); addEventListener("keydown", function (e) { switch (e.key.toUpperCase()) { case "ARROWLEFT": vid.currentTime -= 2; break; case "ARROWRIGHT": vid.currentTime += 2; break; default: break; } }); } } } catch (_) {} } } }); observer.observe(document.documentElement, { childList: true, subtree: true, }); function padTo2Digits(num) { return num.toString().padStart(2, "0"); } function updateVidElemWithRAF() { updateVidElem(); requestAnimationFrame(updateVidElemWithRAF); } function navigationButtonDown() { const button = document .querySelector("#navigation-button-down") .querySelector("button"); if (button !== null) { button.click(); } } function updateVidElem() { vid = document.querySelector(".html5-video-player video"); reel = vid.closest("ytd-reel-video-renderer"); if (vid === null || reel === null) { return; } // Volume Slide if (reel.querySelector("#byts-vol") === null) { if (document.querySelector("#byts-vol") === null) { const volSlider = document.createElement("input"); volSlider.style.cssText = `user-select: none; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-top: ${ reel.offsetHeight + 5 }px;`; volSlider.type = "range"; volSlider.id = "byts-vol"; volSlider.className = "volslider"; volSlider.name = "vol"; volSlider.min = 0.0; volSlider.max = 1.0; volSlider.step = 0.01; volSlider.value = vid.volume; volSlider.addEventListener("input", function () { vid.volume = this.value; GM_setValue("bytsVolume", this.value); }); reel.appendChild(volSlider); } else { reel.appendChild(document.querySelector("#byts-vol")); } bytsVol = document.querySelector("#byts-vol"); } else { try { if (constantVolume) { vid.volume = document.querySelector("#byts-vol").value; } document.querySelector("#byts-vol").value = vid.volume; } catch (_) {} } bytsVol.style.marginTop = `${reel.offsetHeight + 5}px`; // Progress Bar if (reel.querySelector("#byts-progbar") === null) { const progressbar = reel.querySelector("#progress-bar"); if (progressbar !== null) { progressbar.remove(); } if (document.querySelector("#byts-progbar") === null) { const progbar = document.createElement("div"); progbar.id = "byts-progbar"; progbar.style.cssText = "user-select: none; cursor: pointer; width: 98%; height: 6px; background-color: #343434; position: absolute; margin-top: 846px; border-radius: 10px"; reel.appendChild(progbar); } else { reel.appendChild(document.querySelector("#byts-progbar")); } progbar = document.querySelector("#byts-progbar"); progbar.addEventListener("mousemove", (e) => { if (seekMouseDown) { vid.currentTime = ((e.offsetX * 1) / reel.offsetWidth) * vid.duration; } }); progbar.addEventListener("mousedown", () => { seekMouseDown = true; }); progbar.addEventListener("mouseleave", () => { seekMouseDown = false; }); progbar.addEventListener("mouseup", (e) => { seekMouseDown = false; vid.currentTime = ((e.offsetX * 1) / reel.offsetWidth) * vid.duration; }); } // Progress Bar (Inner Red Bar) progressTime = (vid.currentTime / vid.duration) * 100; if ((progress = progbar.querySelector("#byts-progress")) === null) { progress = document.createElement("div"); progress.id = "byts-progress"; progress.style.cssText = `user-select: none; background-color: #FF0000; height: 100%; border-radius: 10px; width: ${progressTime}%;`; progress.addEventListener("mouseup", (e) => { const selected_val = (e.offsetX * 1) / reel.offsetWidth; vid.currentTime = selected_val * vid.duration; }); progbar.appendChild(progress); } else { progress.style.width = `${progressTime}%`; } // Time Info let durSecs = Math.floor(vid.duration); let durMinutes = Math.floor(durSecs / 60); let durSeconds = durSecs % 60; let curSecs = Math.floor(vid.currentTime); if ( curSecs != lastCurSeconds || reel.querySelector("#byts-timeinfo") === null ) { lastCurSeconds = curSecs; let curMinutes = Math.floor(curSecs / 60); let curSeconds = curSecs % 60; // TimeInfo Element if (reel.querySelector("#byts-timeinfo") === null) { if (document.querySelector("#byts-timeinfo") === null) { const 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;`; const timeInfoTextDiv = document.createElement("div"); timeInfoTextDiv.id = "byts-timeinfo-textdiv"; timeInfoTextDiv.style.cssText = `display: flex; margin-right: 5px; margin-top: 4px; color: white; font-size: 1.2rem;`; timeInfoTextDiv.textContent = `${curMinutes}:${padTo2Digits( curSeconds )} / ${durMinutes}:${padTo2Digits(durSeconds)}`; timeInfo.appendChild(timeInfoTextDiv); reel.appendChild(timeInfo); } else { reel.appendChild(document.querySelector("#byts-timeinfo")); } bytsTimeInfo = document.querySelector("#byts-timeinfo"); } document.querySelector( "#byts-timeinfo-textdiv" ).textContent = `${curMinutes}:${padTo2Digits( curSeconds )} / ${durMinutes}:${padTo2Digits(durSeconds)}`; } bytsTimeInfo.style.marginTop = `${reel.offsetHeight + 2}px`; // AutoScroll if (reel.querySelector("#byts-autoscroll-div") === null) { if (document.querySelector("#byts-autoscroll-div") === null) { let astc = ""; if (autoScrollVal) { astc = " checked"; } const 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"; autoscrollInput = document.createElement("input"); autoscrollInput.id = "byts-autoscroll-input"; autoscrollInput.type = "checkbox"; autoscrollInput.checked = autoScrollVal; autoscrollInput.addEventListener("input", function () { autoScrollVal = this.checked; GM_setValue("bytsAutoscroll", this.checked); }); const autoscrollSlider = document.createElement("span"); autoscrollSlider.className = "slider round"; autoscrollSwitch.appendChild(autoscrollInput); autoscrollSwitch.appendChild(autoscrollSlider); autoscrollDiv.appendChild(autoscrollSwitch); reel.appendChild(autoscrollDiv); } else { reel.appendChild(document.querySelector("#byts-autoscroll-div")); } bytsVol = document.querySelector("#byts-autoscroll-div"); } else { if (autoScrollVal == true) { vid.removeAttribute("loop"); vid.removeEventListener("ended", navigationButtonDown); vid.addEventListener("ended", navigationButtonDown); } else { vid.setAttribute("loop", true); vid.removeEventListener("ended", navigationButtonDown); } } bytsVol.style.marginTop = `${reel.offsetHeight + 2}px`; }