在YouTube的右下部区域添加一个快速速度和音量界面,而不干扰现有控件。
当前为
// ==UserScript==
// @name YouTube Quick Speed & Volume Interface(Capsule Style)
// @name:zh-TW YouTube 快速倍速與音量控制介面(膠囊樣式)
// @name:zh-CN YouTube 快速倍速与音量控制界面(胶囊样式)
// @namespace https://twitter.com/CobleeH
// @version 5.52
// @description Add a quick speed and volume interface to YouTube's middle-bottom area without interfering with existing controls.
// @description:zh-TW 在YouTube的右下部區域添加一個快速速度和音量界面,而不干擾現有控件。
// @description:zh-CN 在YouTube的右下部区域添加一个快速速度和音量界面,而不干扰现有控件。
// @author CobleeH
// @match https://www.youtube.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const speeds = [0.5, 1, 1.5, 2, 3];
const volumes = [0, 0.15, 0.35, 0.65, 1];
const UNIFIED_MIN_WIDTH = '180px';
const CAPSULE_BACKGROUND_COLOR = 'rgba(40, 40, 40, 0.4)';
// 定義高亮和懸停時的圓角半徑,使其呈現膠囊形狀
const HIGHLIGHT_BORDER_RADIUS = '9999px';
// 定義兩種模式下的 bottom 值
const BOTTOM_NORMAL = '85px';
const BOTTOM_FULLSCREEN = '170px'; // 避開分享按鈕的高度
// 水平對齊修正
const RIGHT_OFFSET = '25px';
function createControlContainer() {
const container = document.createElement('div');
container.classList.add('ytp-control-container-custom');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'stretch';
container.style.position = 'absolute';
container.style.right = RIGHT_OFFSET;
container.style.bottom = BOTTOM_NORMAL;
container.style.zIndex = '9999';
container.style.opacity = '0';
container.style.pointerEvents = 'none';
container.style.transition = 'opacity 0.2s ease-out, bottom 0.2s ease-out';
const volumeOptions = createVolumeOptions();
const speedOptions = createSpeedOptions();
container.appendChild(volumeOptions);
container.appendChild(speedOptions);
return container;
}
function createOptionElement(text) {
const option = document.createElement('div');
option.classList.add('ytp-option-custom');
option.innerText = text;
option.style.cursor = 'pointer';
option.style.margin = '0 3px';
option.style.flexGrow = '1';
option.style.textAlign = 'center';
option.style.padding = '3px 6px';
// 【修正點 1】預設圓角也改為膠囊形,以配合懸停效果
option.style.borderRadius = HIGHLIGHT_BORDER_RADIUS;
option.style.transition = 'background-color 0.1s ease, color 0.1s ease, font-weight 0.1s ease';
option.addEventListener('mouseenter', () => {
// 只有在未被高亮選中時,才顯示 hover 效果
if (option.style.backgroundColor !== 'rgb(255, 255, 255)' && option.style.backgroundColor !== 'rgb(255,255,255)') {
option.style.backgroundColor = 'rgba(255, 255, 255, 0.25)';
}
});
option.addEventListener('mouseleave', () => {
// 只有在未被高亮選中時,才移除 hover 效果
if (option.style.backgroundColor !== 'rgb(255, 255, 255)' && option.style.backgroundColor !== 'rgb(255,255,255)') {
option.style.backgroundColor = 'transparent';
}
});
return option;
}
// 此函數同時負責高亮選中的按鈕和取消高亮其他按鈕
function highlightOption(selectedOption, selector) {
const options = document.querySelectorAll(selector);
options.forEach(option => {
option.style.color = '#fff';
option.style.fontWeight = 'normal';
option.style.backgroundColor = 'transparent';
// 【修正點 2】取消高亮時,圓角保持膠囊形狀
option.style.borderRadius = HIGHLIGHT_BORDER_RADIUS;
});
selectedOption.style.backgroundColor = '#fff';
selectedOption.style.color = '#000';
selectedOption.style.fontWeight = 'bold';
// 這裡確保選中時是膠囊形
selectedOption.style.borderRadius = HIGHLIGHT_BORDER_RADIUS;
}
function createVolumeOptions() {
const volumeContainer = document.createElement('div');
volumeContainer.classList.add('ytp-volume-options-custom');
volumeContainer.style.display = 'flex';
volumeContainer.style.alignItems = 'center';
volumeContainer.style.marginBottom = '5px';
// 套用統一寬度
volumeContainer.style.minWidth = UNIFIED_MIN_WIDTH;
// --- 膠囊樣式 ---
volumeContainer.style.background = CAPSULE_BACKGROUND_COLOR;
volumeContainer.style.borderRadius = '9999px';
volumeContainer.style.padding = '4px 10px';
// --- 膠囊樣式 END ---
const label = document.createElement('span');
label.innerText = 'Vol';
label.style.marginRight = '8px';
label.style.fontWeight = 'bold';
volumeContainer.appendChild(label);
volumes.forEach(volume => {
const option = createOptionElement((volume * 100) + '%');
option.addEventListener('click', () => {
const video = document.querySelector('video');
if (video) {
video.volume = volume;
highlightOption(option, '.ytp-volume-options-custom .ytp-option-custom');
}
});
volumeContainer.appendChild(option);
});
return volumeContainer;
}
function createSpeedOptions() {
const speedContainer = document.createElement('div');
speedContainer.classList.add('ytp-speed-options-custom');
speedContainer.style.display = 'flex';
speedContainer.style.alignItems = 'center';
// 套用統一寬度
speedContainer.style.minWidth = UNIFIED_MIN_WIDTH;
// --- 膠囊樣式 ---
speedContainer.style.background = CAPSULE_BACKGROUND_COLOR;
speedContainer.style.borderRadius = '9999px';
speedContainer.style.padding = '4px 10px';
// --- 膠囊樣式 END ---
const label = document.createElement('span');
label.innerText = 'Spd';
label.style.marginRight = '8px';
label.style.fontWeight = 'bold';
speedContainer.appendChild(label);
speeds.forEach(speed => {
const option = createOptionElement(speed + 'x');
option.addEventListener('click', () => {
const video = document.querySelector('video');
if (video) {
video.playbackRate = speed;
highlightOption(option, '.ytp-speed-options-custom .ytp-option-custom');
}
});
speedContainer.appendChild(option);
});
return speedContainer;
}
// 設定初始高亮並添加事件監聽器,修復切換影片不同步的問題
function setupInitialStateAndSync() {
const video = document.querySelector('video');
if (!video) return;
// --- 核心同步邏輯 ---
// 速度同步函數
const syncSpeed = () => {
const newSpeed = video.playbackRate;
const speedOptions = document.querySelectorAll('.ytp-speed-options-custom .ytp-option-custom');
speedOptions.forEach(option => {
const speedValue = parseFloat(option.innerText.replace('x', ''));
if (Math.abs(speedValue - newSpeed) < 0.001) {
highlightOption(option, '.ytp-speed-options-custom .ytp-option-custom');
}
});
};
// 音量同步函數
const syncVolume = () => {
const newVolume = video.volume;
const volumeOptions = document.querySelectorAll('.ytp-volume-options-custom .ytp-option-custom');
volumeOptions.forEach(option => {
const volumeValue = parseFloat(option.innerText.replace('%', '')) / 100;
// 考慮到 YouTube 影片的 mute 狀態,當影片靜音時,音量 slider 會在 0 處
const effectiveVolume = video.muted ? 0 : newVolume;
if (Math.abs(volumeValue - effectiveVolume) < 0.001) {
highlightOption(option, '.ytp-volume-options-custom .ytp-option-custom');
}
});
};
// 1. 執行初始高亮 (新影片載入時執行)
syncSpeed();
syncVolume();
// 2. 監聽原生播放器狀態變化
video.addEventListener('ratechange', syncSpeed);
video.addEventListener('volumechange', syncVolume);
}
// 動態調整膠囊位置
function updatePosition(player, controlContainer) {
if (!controlContainer) return;
const isFullscreen = player.classList.contains('ytp-fullscreen');
// 在劇院模式下,播放器容器寬度與視窗寬度一致,且原生浮動 UI 也會出現
const isTheaterMode = document.querySelector('ytd-watch-flexy[theater]') || player.classList.contains('ytp-autohide');
// 在全螢幕或劇院模式下使用更高的 BOTTOM 值
if (isFullscreen || isTheaterMode) {
controlContainer.style.bottom = BOTTOM_FULLSCREEN;
} else {
controlContainer.style.bottom = BOTTOM_NORMAL;
}
}
function setupAutoHideSync(player, controlContainer) {
// 確保控制項的顯示/隱藏與原生控制條同步
const showControls = () => {
controlContainer.style.opacity = '1';
controlContainer.style.pointerEvents = 'auto';
};
const hideControls = () => {
controlContainer.style.opacity = '0';
controlContainer.style.pointerEvents = 'none';
};
// 觀察器:監聽播放器 class 變化,並同時更新位置
const observer = new MutationObserver(() => {
updatePosition(player, controlContainer); // 每次類別改變時都更新位置
if (player.classList.contains('ytp-autohide')) {
hideControls();
} else {
showControls();
}
});
observer.observe(player, { attributes: true, attributeFilter: ['class'] });
// 初始執行一次
updatePosition(player, controlContainer);
if (player.classList.contains('ytp-autohide')) {
hideControls();
} else {
showControls();
}
}
function insertControls() {
const player = document.querySelector('.html5-video-player');
if (!player) return;
// 清理舊的控制容器,確保每次切換影片都會重新插入
let existingContainer = document.querySelector('.ytp-control-container-custom');
if (existingContainer) {
existingContainer.remove();
}
const controlContainer = createControlContainer();
player.appendChild(controlContainer);
setupInitialStateAndSync(); // 呼叫同步函數
setupAutoHideSync(player, controlContainer); // 處理位置更新
}
// 觀察器與載入事件
const mainObserver = new MutationObserver(() => {
if (document.querySelector('.html5-video-player') && !document.querySelector('.ytp-control-container-custom')) {
insertControls();
}
});
mainObserver.observe(document.body, { childList: true, subtree: true });
window.addEventListener('load', insertControls);
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
// 延遲 500ms 確保 YouTube 播放器已載入新影片的屬性
setTimeout(insertControls, 500);
}
}).observe(document, {subtree: true, childList: true});
})();