// ==UserScript== // @name YouTube Quick Speed & Volume Interface(Capsule Style) // @name:zh-TW YouTube 快速倍速與音量控制介面(膠囊樣式) // @name:zh-CN YouTube 快速倍速与音量控制界面(胶囊样式) // @namespace https://twitter.com/CobleeH // @version 3.0 // @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 / Gemini Revision // @match https://www.youtube.com/* // @grant none // @license MIT // @downloadURL none // ==/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)'; // 定義兩種模式下的 bottom 值 const BOTTOM_NORMAL = '85px'; const BOTTOM_FULLSCREEN = '170px'; // 避開分享按鈕的高度 // 🚀 水平對齊修正:將 right 值從 15px 調整到 25px,以更好地貼齊浮動 UI 的右邊緣 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; // 使用修正後的 right 值         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';         option.style.borderRadius = '4px';         option.style.transition = 'background-color 0.1s ease, color 0.1s ease';         option.addEventListener('mouseenter', () => {             if (option.style.backgroundColor !== 'rgb(255, 255, 255)') {                 option.style.backgroundColor = 'rgba(255, 255, 255, 0.25)';             }         });         option.addEventListener('mouseleave', () => {             if (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';         });         selectedOption.style.backgroundColor = '#fff';         selectedOption.style.color = '#000';         selectedOption.style.fontWeight = 'bold';     }     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 = '3px';         // 套用統一寬度         volumeContainer.style.minWidth = UNIFIED_MIN_WIDTH;         // --- 膠囊樣式 ---         volumeContainer.style.background = CAPSULE_BACKGROUND_COLOR;         volumeContainer.style.borderRadius = '9999px';         volumeContainer.style.padding = '4px 10px';         //volumeContainer.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)';陰影效果         // --- 膠囊樣式 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';         //speedContainer.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.5)';陰影效果         // --- 膠囊樣式 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}); })();