Greasy Fork is available in English.
为Youtube Shorts提供更多的控制功能,包括音量控制,进度条,自动滚动,快捷键等等。
当前为
// ==UserScript==
// @name Better Youtube Shorts
// @name:zh-CN 更好的Youtube Shorts
// @name:zh-TW 更好的Youtube Shorts
// @name:ja より良いYoutube Shorts
// @namespace Violentmonkey Scripts
// @version 1.2.5
// @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
// ==/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%;
}`
);
let seekMouseDown = false;
let lastCurSeconds = 0;
let shortsAndPlayerReady = 0;
let autoScrollVal = GM_getValue("autoscroll", true);
let constantVolume = GM_getValue("constantVolume", false);
GM_registerMenuCommand(
`Constant Volume: ${constantVolume ? "On" : "Off"}`,
function () {
constantVolume = !constantVolume;
GM_setValue("constantVolume", constantVolume);
location.reload();
}
);
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
let shortsReady = node.tagName === "YTD-SHORTS";
let videoPlayerReady =
typeof node.className === "string" &&
node.className.includes("html5-video-player");
if (shortsReady || videoPlayerReady) {
shortsAndPlayerReady++;
if (videoPlayerReady) {
let videoPlayer = document.querySelector(".html5-video-player video");
addShortcuts(videoPlayer);
}
if (shortsAndPlayerReady === 2) {
observer.disconnect();
updateVidElemWithRAF();
}
}
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
function addShortcuts(videoPlayer) {
addEventListener("keydown", function (e) {
switch (e.key.toUpperCase()) {
case "ARROWLEFT":
videoPlayer.currentTime -= 2;
break;
case "ARROWRIGHT":
videoPlayer.currentTime += 2;
break;
default:
break;
}
});
}
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() {
const videoPlayer = document.querySelector(".html5-video-player video");
const reelVideoRenderer = videoPlayer.closest("ytd-reel-video-renderer");
if (videoPlayer === null || reelVideoRenderer === null) {
return;
}
// Volume Slider
let volumeSlider = document.querySelector("#byts-vol");
if (reelVideoRenderer.querySelector("#byts-vol") === null) {
if (volumeSlider === null) {
volumeSlider = document.createElement("input");
volumeSlider.style.cssText = `user-select: none; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-top: ${
reelVideoRenderer.offsetHeight + 5
}px;`;
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 = videoPlayer.volume;
volumeSlider.addEventListener("input", function () {
videoPlayer.volume = this.value;
GM_setValue("bytsVolume", this.value);
});
}
reelVideoRenderer.appendChild(volumeSlider);
}
if (constantVolume) {
videoPlayer.volume = document.querySelector("#byts-vol").value;
}
document.querySelector("#byts-vol").value = videoPlayer.volume;
volumeSlider.style.marginTop = `${reelVideoRenderer.offsetHeight + 5}px`;
// Progress Bar
let progressBar = document.querySelector("#byts-progbar");
if (reelVideoRenderer.querySelector("#byts-progbar") === null) {
const builtinProgressbar = reelVideoRenderer.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; margin-top: 846px; border-radius: 10px";
}
reelVideoRenderer.appendChild(progressBar);
progressBar.addEventListener("mousemove", (e) => {
if (seekMouseDown) {
videoPlayer.currentTime =
((e.offsetX * 1) / reelVideoRenderer.offsetWidth) *
videoPlayer.duration;
}
});
progressBar.addEventListener("mousedown", () => {
seekMouseDown = true;
});
progressBar.addEventListener("mouseleave", () => {
seekMouseDown = false;
});
progressBar.addEventListener("mouseup", (e) => {
seekMouseDown = false;
videoPlayer.currentTime =
((e.offsetX * 1) / reelVideoRenderer.offsetWidth) *
videoPlayer.duration;
});
}
// Progress Bar (Inner Red Bar)
let progressTime = (videoPlayer.currentTime / videoPlayer.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}%;`;
InnerProgressBar.addEventListener("mouseup", (e) => {
const selected_val = (e.offsetX * 1) / reelVideoRenderer.offsetWidth;
videoPlayer.currentTime = selected_val * videoPlayer.duration;
});
progressBar.appendChild(InnerProgressBar);
}
InnerProgressBar.style.width = `${progressTime}%`;
// Time Info
let durSecs = Math.floor(videoPlayer.duration);
let durMinutes = Math.floor(durSecs / 60);
let durSeconds = durSecs % 60;
let curSecs = Math.floor(videoPlayer.currentTime);
let timeInfo = document.querySelector("#byts-timeinfo");
let timeInfoTextDiv = document.querySelector("#byts-timeinfo-textdiv");
if (
curSecs != lastCurSeconds ||
reelVideoRenderer.querySelector("#byts-timeinfo") === null
) {
lastCurSeconds = curSecs;
let curMinutes = Math.floor(curSecs / 60);
let curSeconds = curSecs % 60;
if (reelVideoRenderer.querySelector("#byts-timeinfo") === null) {
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: ${
reelVideoRenderer.offsetHeight + 2
}px;`;
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);
}
reelVideoRenderer.appendChild(timeInfo);
}
timeInfoTextDiv.textContent = `${curMinutes}:${padTo2Digits(
curSeconds
)} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
}
timeInfo.style.marginTop = `${reelVideoRenderer.offsetHeight + 2}px`;
// AutoScroll
let autoScrollDiv = document.querySelector("#byts-autoscroll-div");
if (reelVideoRenderer.querySelector("#byts-autoscroll-div") === null) {
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: ${
reelVideoRenderer.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 = autoScrollVal;
autoscrollInput.addEventListener("input", function () {
autoScrollVal = 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);
}
reelVideoRenderer.appendChild(autoScrollDiv);
}
if (autoScrollVal === true) {
videoPlayer.removeAttribute("loop");
videoPlayer.removeEventListener("ended", navigationButtonDown);
videoPlayer.addEventListener("ended", navigationButtonDown);
} else {
videoPlayer.setAttribute("loop", true);
videoPlayer.removeEventListener("ended", navigationButtonDown);
}
autoScrollDiv.style.marginTop = `${reelVideoRenderer.offsetHeight + 2}px`;
}