// ==UserScript==
// @name Better Youtube Shorts
// @name:zh-CN 更好的 Youtube Shorts
// @name:zh-TW 更好的 Youtube Shorts
// @namespace Violentmonkey Scripts
// @version 2.2.8
// @description Provide more control functions for YouTube Shorts, including automatic/manual redirection to corresponding video pages, volume control, progress bar, auto scrolling, shortcut keys, and more.
// @description:zh-CN 为 Youtube Shorts提供更多的控制功能,包括自动/手动跳转到对应视频页面,音量控制,进度条,自动滚动,快捷键等等。
// @description:zh-TW 為 Youtube Shorts提供更多的控制功能,包括自動/手動跳轉到對應影片頁面,音量控制,進度條,自動滾動,快捷鍵等等。
// @author Meriel
// @match *://*.youtube.com/*
// @exclude *://music.youtube.com/*
// @run-at document-start
// @grant GM.addStyle
// @grant GM.registerMenuCommand
// @grant GM.getValue
// @grant GM.setValue
// @grant GM_info
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @homepageURL https://github.com/MerielVaren/better-youtube-shorts
// @supportURL https://github.com/MerielVaren/better-youtube-shorts/issues
// ==/UserScript==
(async () => {
const shouldNotifyUserAboutChanges = false;
const userLanguage = navigator.language || navigator.userLanguage;
const i18nText = {
zhSimplified: {
closeText: `<br>双击关闭此消息👆`,
updateText: `BTYS 版本 ${GM_info.script.version}<br>
我们完善了语言的国际化💬<br>
现在支持简体中文,繁体中文和英文🌎<br>
如果需要支持更多语言<br>
请在tampemonkey的反馈区留言📝<br>
`,
newInstallationText: `
欢迎使用 Better YouTube Shorts🎉<br>
请检查 Tampermonkey 菜单中的设置🛠️<br>
里面还有更多功能📢<br>
下面是快捷键的说明👇<br>
<br>
箭头上/下: 向上/向下滚动<br>
箭头左/右: 后退/前进<br>
Shift + 箭头上/左: 音量增加/减少<br>
Shift + 箭头下/右: 音量减少/增加<br>
Alt + 回车: 切换全屏<br>
Alt + W: 在当前标签页中打开观看页面<br>
0~9: 跳转到对应的进度<br>
`,
on: "开启",
off: "关闭",
constantVolume: "恒定音量",
operationMode: "快捷键",
videoMode: "视频操作模式",
shortsMode: "短视频操作模式",
continueFromLastCheckpoint: "从上次检查点继续",
off: "关闭",
temporary: "临时保存",
permanent: "永久保存",
loopPlayback: "循环播放",
openWatchInCurrentTab: "在当前标签页中打开对应视频",
doubleClickToFullscreen: "双击全屏",
progressBarStyle: "进度条样式",
original: "原始",
custom: "自定义",
autoScroll: "自动滚动",
shortsAutoSwitchToVideo: "短视频自动切换到对应视频",
},
zhTraditional: {
closeText: `<br>雙擊關閉此消息👆`,
updateText: `BTYS 版本 ${GM_info.script.version}<br>
我們完善了語言的國際化💬<br>
現在支持繁體中文,簡體中文以及英文🌎<br>
如果需要支持更多語言<br>
請在tampemonkey的反饋區留言📝<br>
`,
newInstallationText: `
歡迎使用 Better YouTube Shorts🎉<br>
請檢查 Tampermonkey 菜單中的設置🛠️<br>
裡面還有更多功能📢<br>
下面是快捷鍵的說明👇<br>
<br>
箭頭上/下: 向上/向下滾動<br>
箭頭左/右: 後退/前進<br>
Shift + 箭頭上/左: 音量增加/減少<br>
Shift + 箭頭下/右: 音量減少/增加<br>
Alt + 回車: 切換全屏<br>
Alt + W: 在當前標籤頁中打開觀看頁面<br>
0~9: 跳轉到對應的進度<br>
`,
on: "開啟",
off: "關閉",
constantVolume: "恆定音量",
operationMode: "快捷鍵",
videoMode: "視頻操作模式",
shortsMode: "短視頻操作模式",
continueFromLastCheckpoint: "從上次檢查點繼續",
off: "關閉",
temporary: "臨時保存",
permanent: "永久保存",
loopPlayback: "循環播放",
openWatchInCurrentTab: "在當前標籤頁中打開對應視頻",
doubleClickToFullscreen: "雙擊全屏",
progressBarStyle: "進度條樣式",
original: "原始",
custom: "自定義",
autoScroll: "自動滾動",
shortsAutoSwitchToVideo: "短視頻自動切換到對應視頻",
},
en: {
closeText: `<br>Double click to close this message👆`,
updateText: `BTYS Version ${GM_info.script.version}<br>
We have improved the internationalization of languages💬<br>
Now supports English, Chinese Simplified, and Chinese Traditional🌎<br>
If you need support for more languages<br>
Please leave a message in the tampemonkey feedback area📝<br>
`,
newInstallationText: `
Welcome to Better YouTube Shorts🎉<br>
Please check the settings in the Tampermonkey menu🛠️<br>
There are more features in it📢<br>
Below is the explanation of the shortcut keys👇<br>
<br>
Arrow Up/Down: Scroll up/down<br>
Arrow Left/Right: Seek backward/forward<br>
Shift + Arrow Up/Left: Volume up/backward<br>
Shift + Arrow Down/Right: Volume down/forward<br>
Alt + Enter: Toggle fullscreen<br>
Alt + W: Open watch page in current tab<br>
0~9: Jump to the corresponding progress<br>
`,
on: "on",
off: "off",
constantVolume: "Constant Volume",
operationMode: "Operation Mode",
videoMode: "video operation mode",
shortsMode: "shorts operation mode",
continueFromLastCheckpoint: "Continue From Last Checkpoint",
off: "off",
temporary: "temporary",
permanent: "permanent",
loopPlayback: "Loop Playback",
openWatchInCurrentTab: "Open Watch in Current Tab",
doubleClickToFullscreen: "Double Click to Fullscreen",
progressBarStyle: "Progress Bar Style",
original: "original",
custom: "custom",
autoScroll: "Auto Scroll",
shortsAutoSwitchToVideo: "Shorts Auto Switch To Video",
},
};
const i18n = userLanguage.includes("zh")
? userLanguage === "zh-CN" || userLanguage === "zh-SG"
? i18nText.zhSimplified
: i18nText.zhTraditional
: i18nText.en;
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
let currentUrl = "";
const once = (fn) => {
let done = false;
let result;
return async (...args) => {
if (done) return result;
done = true;
result = await fn(...args);
return result;
};
};
const closeText = i18n.closeText;
let updateText = i18n.updateText;
let newInstallationText = i18n.newInstallationText;
updateText += closeText;
newInstallationText += closeText;
const higherVersion = (v1, v2) => {
const v1Arr = v1.split(".");
const v2Arr = v2.split(".");
for (let i = 0; i < v1Arr.length; i++) {
if (v1Arr[i] > v2Arr[i]) {
return true;
} else if (v1Arr[i] < v2Arr[i]) {
return false;
}
}
return false;
};
const version = await GM.getValue("version");
let interval;
const checkVideoPaused = (video, waitTime = 100) => {
if (!video.paused) {
video.pause();
interval = setTimeout(() => checkVideoPaused(video, waitTime), waitTime);
} else {
clearTimeout(interval);
}
};
const newInstallation = once(async (reel, video) => {
if (!version) {
if (!interval) {
interval = setTimeout(() => checkVideoPaused(video, 100), 100);
}
GM.setValue("version", GM_info.script.version);
const info = document.createElement("div");
info.style.cssText = `position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.5); z-index: 999; margin: 5px 0; color: black; font-size: 2rem; font-weight: bold; text-align: center; border-radius: 10px; padding: 10px; box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.5); transition: 0.5s;`;
const infoText = document.createElement("div");
infoText.style.cssText = `background-color: white; padding: 10px; border-radius: 10px; font-size: 1.5rem;`;
infoText.innerHTML = newInstallationText;
info.appendChild(infoText);
reel.appendChild(info);
info.addEventListener("dblclick", () => {
info.remove();
video.play();
});
}
});
const update = once(async (reel, video) => {
GM.setValue("version", GM_info.script.version);
if (
typeof version === "string" &&
higherVersion(GM_info.script.version, version) &&
shouldNotifyUserAboutChanges
) {
if (!interval) {
interval = setTimeout(() => checkVideoPaused(video, 100), 100);
}
GM.setValue("version", GM_info.script.version);
const info = document.createElement("div");
info.style.cssText = `position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.5); z-index: 999; margin: 5px 0; color: black; font-size: 2rem; font-weight: bold; text-align: center; border-radius: 10px; padding: 10px; box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.5); transition: 0.5s;`;
const infoText = document.createElement("div");
infoText.style.cssText = `background-color: white; padding: 10px; border-radius: 10px; font-size: 1.5rem;`;
infoText.innerHTML = updateText;
info.appendChild(infoText);
reel.appendChild(info);
info.addEventListener("dblclick", () => {
info.remove();
video.play();
});
}
});
let shortsAutoSwitchToVideo = await GM.getValue("shortsAutoSwitchToVideo");
if (shortsAutoSwitchToVideo === void 0) {
shortsAutoSwitchToVideo = false;
GM.setValue("shortsAutoSwitchToVideo", shortsAutoSwitchToVideo);
}
GM.registerMenuCommand(
`${i18n.shortsAutoSwitchToVideo}: ${
shortsAutoSwitchToVideo ? i18n.on : i18n.off
}`,
() => {
shortsAutoSwitchToVideo = !shortsAutoSwitchToVideo;
GM.setValue("shortsAutoSwitchToVideo", shortsAutoSwitchToVideo).then(
() => (location.href = location.href.replace("watch?v=", "shorts/"))
);
}
);
if (shortsAutoSwitchToVideo) {
if (window.location.pathname.match("/shorts/.+")) {
window.location.replace(
"https://www.youtube.com/watch?v=" +
window.location.pathname.split("/shorts/").pop()
);
}
document.addEventListener("yt-navigate-start", (event) => {
const url = event.detail.url.split("/shorts/");
if (url.length > 1) {
window.location.replace("https://www.youtube.com/watch?v=" + url.pop());
}
});
return;
}
const initialize = once(async () => {
GM.addStyle(
`input[type="range"].volslider {
height: 12px;
-webkit-appearance: none;
-moz-appearance: none; /* Firefox */
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: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
border-radius: 25px;
}
input[type="range"].volslider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
margin-top: -2px;
border-radius: 50%;
background: ${isDarkMode ? "white" : "black"};
}
/* Firefox */
input[type="range"].volslider::-moz-range-track {
height: 8px;
cursor: pointer;
box-shadow: 0px 0px 0px #000000;
background: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
border-radius: 25px;
}
input[type="range"].volslider::-moz-range-thumb {
width: 12px;
height: 12px;
border: none;
border-radius: 50%;
background: ${isDarkMode ? "white" : "black"};
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 12px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 0px;
bottom: 0px;
background-color: ${isDarkMode ? "white" : "black"};
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: #ff0000;
}
input:focus + .slider {
box-shadow: 0 0 0px #ff0000;
}
input:checked + .slider:before {
-webkit-transform: translateX(29px);
-ms-transform: translateX(29px);
transform: translateX(29px);
}
/* Rounded sliders */
.slider.round {
border-radius: 12px;
}
.slider.round:before {
border-radius: 50%;
}
/* red progress bar */
#byts-progbar:hover #byts-progress::after,
#byts-progbar.show-dot #byts-progress::after {
content: '';
position: absolute;
top: 50%;
right: 0;
transform: translate(50%, -50%);
width: 15px;
height: 15px;
background-color: #FF0000;
border-radius: 50%;
display: block;
}
`
);
let seekMouseDown = false;
let lastCurSeconds = 0;
let video = null;
let autoScroll = await GM.getValue("autoScroll");
let loopPlayback = await GM.getValue("loopPlayback");
let constantVolume = await GM.getValue("constantVolume");
let operationMode = await GM.getValue("operationMode");
let openWatchInCurrentTab = await GM.getValue("openWatchInCurrentTab");
let doubleClickToFullscreen = await GM.getValue("doubleClickToFullscreen");
let progressBarStyle = await GM.getValue("progressBarStyle");
const checkpointStatusEnum = Object.freeze({
[i18n.off]: 0,
[i18n.temporary]: 1,
[i18n.permanent]: 2,
});
let continueFromLastCheckpoint = await GM.getValue(
"continueFromLastCheckpoint"
);
let lastShortsId = "";
if (autoScroll === void 0) {
autoScroll = true;
GM.setValue("autoScroll", autoScroll);
}
if (constantVolume === void 0) {
constantVolume = false;
GM.setValue("constantVolume", constantVolume);
}
if (operationMode === void 0) {
operationMode = "Shorts";
GM.setValue("operationMode", operationMode);
}
if (continueFromLastCheckpoint === void 0) {
continueFromLastCheckpoint = checkpointStatusEnum[i18n.off];
GM.setValue("continueFromLastCheckpoint", continueFromLastCheckpoint);
}
if (loopPlayback === void 0) {
loopPlayback = true;
GM.setValue("loopPlayback", loopPlayback);
}
if (openWatchInCurrentTab === void 0) {
openWatchInCurrentTab = false;
GM.setValue("openWatchInCurrentTab", openWatchInCurrentTab);
}
let shortsCheckpoints;
if (continueFromLastCheckpoint !== checkpointStatusEnum[i18n.off]) {
shortsCheckpoints = await GM.getValue("shortsCheckpoints");
if (
shortsCheckpoints === void 0 ||
continueFromLastCheckpoint === checkpointStatusEnum[i18n.temporary]
) {
shortsCheckpoints = {};
GM.setValue("shortsCheckpoints", shortsCheckpoints);
}
}
if (doubleClickToFullscreen === void 0) {
doubleClickToFullscreen = true;
GM.setValue("doubleClickToFullscreen", doubleClickToFullscreen);
}
if (progressBarStyle === void 0) {
progressBarStyle = "custom";
GM.setValue("progressBarStyle", progressBarStyle);
}
GM.registerMenuCommand(
`${i18n.constantVolume}: ${constantVolume ? i18n.on : i18n.off}`,
() => {
constantVolume = !constantVolume;
GM.setValue("constantVolume", constantVolume).then(() =>
location.reload()
);
}
);
GM.registerMenuCommand(
`${i18n.operationMode}: ${
operationMode === "Video" ? i18n.videoMode : i18n.shortsMode
}`,
() => {
operationMode = operationMode === "Video" ? "Shorts" : "Video";
GM.setValue("operationMode", operationMode).then(() =>
location.reload()
);
}
);
GM.registerMenuCommand(
`${i18n.continueFromLastCheckpoint}: ${Object.keys(checkpointStatusEnum)
.find(
(key) => checkpointStatusEnum[key] === continueFromLastCheckpoint % 3
)
.toLowerCase()}`,
() => {
continueFromLastCheckpoint = (continueFromLastCheckpoint + 1) % 3;
GM.setValue(
"continueFromLastCheckpoint",
continueFromLastCheckpoint
).then(() => location.reload());
}
);
GM.registerMenuCommand(
`${i18n.loopPlayback}: ${loopPlayback ? i18n.on : i18n.off}`,
() => {
loopPlayback = !loopPlayback;
GM.setValue("loopPlayback", loopPlayback).then(() => location.reload());
}
);
GM.registerMenuCommand(
`${i18n.openWatchInCurrentTab}: ${
openWatchInCurrentTab ? i18n.on : i18n.off
}`,
() => {
openWatchInCurrentTab = !openWatchInCurrentTab;
GM.setValue("openWatchInCurrentTab", openWatchInCurrentTab).then(() =>
location.reload()
);
}
);
GM.registerMenuCommand(
`${i18n.doubleClickToFullscreen}: ${
doubleClickToFullscreen ? i18n.on : i18n.off
}`,
() => {
doubleClickToFullscreen = !doubleClickToFullscreen;
GM.setValue("doubleClickToFullscreen", doubleClickToFullscreen).then(
() => location.reload()
);
}
);
GM.registerMenuCommand(
`${i18n.progressBarStyle}: ${
progressBarStyle === "custom" ? i18n.custom : i18n.original
}`,
() => {
progressBarStyle =
progressBarStyle === "custom" ? "original" : "custom";
GM.setValue("progressBarStyle", progressBarStyle).then(() =>
location.reload()
);
}
);
const observer = new MutationObserver(
async (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 = await GM.getValue("volume", 0);
}
addShortcuts();
updateVidElemWithRAF();
break outer;
}
}
}
}
);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
function videoOperationMode(e) {
const volumeSlider = document.getElementById("byts-vol");
if (!e.shiftKey) {
if (
e.key.toUpperCase() === "ARROWUP" ||
e.key.toUpperCase() === "ARROWDOWN"
) {
e.stopPropagation();
e.preventDefault();
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) {
const volumeSlider = document.getElementById("byts-vol");
if (
e.key.toUpperCase() === "ARROWUP" ||
e.key.toUpperCase() === "ARROWDOWN"
) {
e.stopPropagation();
e.preventDefault();
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"
) {
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 handleEvent(e) {
videoOperationMode(e);
if (constantVolume) {
constantVolume = false;
requestAnimationFrame(() => (constantVolume = true));
}
}
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", handleEvent, {
capture: true,
});
observer.disconnect();
}
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
} else {
document.addEventListener(
"keydown",
function (e) {
shortsOperationMode(e);
if (constantVolume) {
constantVolume = false;
requestAnimationFrame(() => (constantVolume = true));
}
},
{
capture: true,
}
);
}
if (doubleClickToFullscreen) {
video.addEventListener("dblclick", function () {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.getElementsByTagName("ytd-app")[0].requestFullscreen();
}
});
}
document.addEventListener("keydown", function (e) {
if (e.altKey && e.key.toUpperCase() === "ENTER") {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.getElementsByTagName("ytd-app")[0].requestFullscreen();
}
}
});
document.addEventListener("keydown", function (e) {
if (e.altKey && e.key.toUpperCase() === "W") {
const watchUrl = location.href.replace("shorts/", "watch?v=");
if (openWatchInCurrentTab) {
window.location.href = watchUrl;
} else {
window.open(watchUrl, "_blank");
}
}
});
document.addEventListener("keydown", function (e) {
if (
(e.key >= "0" && e.key <= "9") ||
(e.code >= "Numpad0" && e.code <= "Numpad9")
) {
video.currentTime = video.duration * (e.key / 10);
}
});
}
function padTo2Digits(num) {
return num.toString().padStart(2, "0");
}
function updateVidElemWithRAF() {
try {
if (currentUrl?.includes("youtube.com/shorts")) {
updateVidElem();
}
} catch (e) {
console.error(e);
}
requestAnimationFrame(updateVidElemWithRAF);
}
function navigationButtonDown() {
document.querySelector("#navigation-button-down button").click();
}
function navigationButtonUp() {
document.querySelector("#navigation-button-up button").click();
}
function setVideoPlaybackTime(event, player) {
const rect = player.getBoundingClientRect();
let offsetX = event.clientX - rect.left;
if (offsetX < 0) {
offsetX = 0;
} else if (offsetX > player.offsetWidth) {
offsetX = player.offsetWidth - 1;
}
let currentTime = (offsetX / player.offsetWidth) * video.duration;
if (currentTime === 0) currentTime = 1e-6;
video.currentTime = currentTime;
}
async function updateVidElem() {
const currentVideo = document.querySelector(
"#shorts-player > div.html5-video-container > video"
);
if (video !== currentVideo) {
video = currentVideo;
}
if (constantVolume) {
video.volume = await GM.getValue("volume", 0);
}
const reel = document.querySelector("ytd-reel-video-renderer[is-active]");
if (reel === null) {
return;
}
if (progressBarStyle === "custom") {
const shortsPlayerControls = document.querySelector(
"#scrubber > ytd-scrubber > shorts-player-controls"
);
const scrubber = document.getElementById("scrubber");
shortsPlayerControls?.remove();
scrubber?.remove();
}
update(reel, video);
newInstallation(reel, video);
if (continueFromLastCheckpoint !== checkpointStatusEnum[i18n.off]) {
const currentSec = Math.floor(video.currentTime);
const shortsUrlList = location.href.split("/");
if (!shortsUrlList.includes("shorts")) return;
const shortsId = shortsUrlList.pop();
if (shortsId !== lastShortsId) {
lastShortsId = shortsId;
const checkpoint = shortsCheckpoints[shortsId] || 1e-6;
video.pause();
if (checkpoint + 1 >= video.duration) {
video.currentTime = 1e-6;
} else {
video.currentTime = checkpoint;
}
video.play();
}
if (currentSec !== lastCurSeconds && video.currentTime !== 0) {
lastCurSeconds = currentSec;
shortsCheckpoints[shortsId] = currentSec;
GM.setValue("shortsCheckpoints", shortsCheckpoints);
}
}
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 = document.getElementById("byts-vol-div");
let volumeSlider = document.getElementById("byts-vol");
let volumeTextDiv = document.getElementById("byts-vol-textdiv");
const reelVolumeSliderDiv = reel.querySelector("#byts-vol-div");
if (reelVolumeSliderDiv === null) {
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}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: ${
isDarkMode ? "white" : "black"
}; font-size: 1.2rem; margin-left: ${volumeSlider.offsetWidth + 1}px`;
volumeTextDiv.textContent = `${(
video.volume.toFixed(2) * 100
).toFixed()}%`;
volumeSliderDiv.appendChild(volumeTextDiv);
}
reel.appendChild(volumeSliderDiv);
}
if (constantVolume) {
video.volume = volumeSlider.value;
}
volumeSlider.value = video.volume;
volumeTextDiv.textContent = `${(
video.volume.toFixed(2) * 100
).toFixed()}%`;
volumeSliderDiv.style.marginTop = `${reel.offsetHeight + 2}px`;
volumeTextDiv.style.marginLeft = `${volumeSlider.offsetWidth + 1}px`;
if (video.muted) {
volumeTextDiv.textContent = "0%";
volumeSlider.value = 0;
} else {
volumeTextDiv.textContent = `${(video.volume * 100).toFixed()}%`;
volumeSlider.value = video.volume;
}
if (progressBarStyle === "custom") {
// Progress Bar
let progressBar = document.getElementById("byts-progbar");
const reelProgressBar = reel.querySelector("#byts-progbar");
if (reelProgressBar === 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: 7px; background-color: #343434; position: absolute; border-radius: 10px; margin-top: ${
reel.offsetHeight - 7
}px;`;
}
reel.appendChild(progressBar);
let wasPausedBeforeDrag = false;
progressBar.addEventListener("mousedown", function (e) {
seekMouseDown = true;
wasPausedBeforeDrag = video.paused;
setVideoPlaybackTime(e, progressBar);
video.pause();
progressBar.classList.add("show-dot");
});
document.addEventListener("mousemove", function (e) {
if (!seekMouseDown) return;
e.preventDefault();
setVideoPlaybackTime(e, progressBar);
if (!video.paused) {
video.pause();
}
e.preventDefault();
});
document.addEventListener("mouseup", function () {
if (!seekMouseDown) return;
seekMouseDown = false;
if (!wasPausedBeforeDrag) {
video.play();
}
progressBar.classList.remove("show-dot");
});
}
progressBar.style.marginTop = `${reel.offsetHeight - 7}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}%;
position: relative;
`;
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 = document.getElementById("byts-timeinfo");
let timeInfoText = document.getElementById("byts-timeinfo-textdiv");
const reelTimeInfo = reel.querySelector("#byts-timeinfo");
if (!Number.isNaN(durSecs) && reelTimeInfo !== null) {
timeInfoText.textContent = `${Math.floor(curSecs / 60)}:${padTo2Digits(
curSecs % 60
)} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
}
if (curSecs !== lastCurSeconds || reelTimeInfo === null) {
lastCurSeconds = curSecs;
const curMinutes = Math.floor(curSecs / 60);
const curSeconds = curSecs % 60;
if (reelTimeInfo === 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: ${
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: ${
isDarkMode ? "white" : "black"
}; 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 = document.getElementById("byts-autoscroll-div");
const reelAutoScrollDiv = reel.querySelector("#byts-autoscroll-div");
if (reelAutoScrollDiv === 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: ${
reel.offsetHeight - 3
}px;`;
const autoScrollTextDiv = document.createElement("div");
autoScrollTextDiv.style.cssText = `display: flex; margin-right: 5px; margin-top: ${
userLanguage.includes("zh") ? "3px" : "4px"
}; color: ${isDarkMode ? "white" : "black"}; font-size: 1.2rem;`;
autoScrollTextDiv.textContent = i18n.autoScroll;
autoScrollDiv.appendChild(autoScrollTextDiv);
const autoScrollSwitch = document.createElement("label");
autoScrollSwitch.className = "switch";
autoScrollSwitch.style.marginTop = "5px";
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 {
if (loopPlayback) {
video.setAttribute("loop", true);
video.removeEventListener("ended", navigationButtonDown);
} else {
video.removeAttribute("loop");
video.removeEventListener("ended", navigationButtonDown);
}
}
autoScrollDiv.style.marginTop = `${reel.offsetHeight - 3}px`;
}
});
const urlChange = (event) => {
const destinationUrl = event?.destination?.url || "";
if (destinationUrl.startsWith("about:blank")) return;
const href = destinationUrl || location.href;
if (href.includes("youtube.com/shorts")) {
if (shortsAutoSwitchToVideo) {
currentUrl = location.href = href.replace("shorts/", "watch?v=");
return;
} else {
currentUrl = href;
initialize();
}
}
};
urlChange();
unsafeWindow?.navigation?.addEventListener("navigate", urlChange);
unsafeWindow.addEventListener("replaceState", urlChange);
unsafeWindow.addEventListener("pushState", urlChange);
unsafeWindow.addEventListener("popState", urlChange);
unsafeWindow.addEventListener("hashchange", urlChange);
})();