Meta reel 音量控制

10.5 增强:加入周期性扫描机制 (每 3 秒),解决在动态页面 (如 Reels) 上 UI 偶尔无法成功跑出来的问题。维持右上角悬浮、最高层级、且点击不暂停。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Meta reel 音量控制 
// @name:zh-TW   Meta reel 音量控制
// @name:zh-CN   Meta reel 音量控制
// @name:en      Meta reel Volume Master
// @name:ja      Meta reel 音量マスター
// @namespace    http://tampermonkey.net/
// @version      10.5
// @description  10.5 增強:加入週期性掃描機制 (每 3 秒),解決在動態頁面 (如 Reels) 上 UI 偶爾無法成功跑出來的問題。維持右上角懸浮、最高層級、且點擊不暫停。
// @description:zh-TW 10.5 增強:加入週期性掃描機制 (每 3 秒),解決在動態頁面 (如 Reels) 上 UI 偶爾無法成功跑出來的問題。維持右上角懸浮、最高層級、且點擊不暫停。
// @description:zh-CN 10.5 增强:加入周期性扫描机制 (每 3 秒),解决在动态页面 (如 Reels) 上 UI 偶尔无法成功跑出来的问题。维持右上角悬浮、最高层级、且点击不暂停。
// @description:en 10.5 Enhancement: Added periodic scan (every 3s) to fix the intermittent failure of UI appearing on dynamic pages like Reels. Maintains top-right, high z-index, and anti-pause clicking.
// @description:ja 10.5 強化:ダイナミックページ(Reelsなど)でUIが稀に出現しない問題を解決するため、定期スキャンメカニズム(3秒ごと)を追加しました。右上、最高レイヤー、一時停止防止の機能を維持。
// @author       You
// @match        *://*.facebook.com/*
// @match        *://*.instagram.com/*
// @match        *://*.threads.net/*
// @match        *://*.threads.com/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================================
    // 設定區域
    // ==========================================================
    const CONFIG = {
        defaultVolume: 0.1, // 預設音量 10%
        showLogs: true,     // 是否顯示除錯紀錄
        ui: {
            opacityIdle: 0.01,   // V4.5: 平常的透明度 (接近完全隱藏)
            opacityHover: 1.0,  // V4.5: 滑鼠移上去的透明度
            positionTop: '10px', // V4.5: 右上角定位
            positionRight: '10px',  // V4.5: 右上角定位
            color: '#ffffff',   // 文字與圖示顏色
            bgColor: 'rgba(0, 0, 0, 0.4)' // 背景顏色設為半透明黑色
        }
    };
    // ==========================================================

    function log(msg) {
        if (CONFIG.showLogs) {
            console.log(`[音量控制 v4.5 - 右上角穩定版] ${msg}`);
        }
    }

    // 注入 CSS 樣式
    function injectStyles() {
        const styleId = 'meta-volume-fixer-style';
        if (document.getElementById(styleId)) return;

        const css = `
            .mvf-overlay {
                position: absolute;
                top: ${CONFIG.ui.positionTop};      /* 右上角定位 */
                right: ${CONFIG.ui.positionRight};   /* 右上角定位 */
                z-index: 2147483647; /* 提升到最高層級 */
                background-color: ${CONFIG.ui.bgColor}; /* 半透明背景 */
                border-radius: 4px;
                padding: 4px 8px;
                display: flex;
                align-items: center;
                gap: 8px;
                opacity: ${CONFIG.ui.opacityIdle}; /* 預設低透明度 */
                transition: opacity 0.2s ease;
                font-family: system-ui, -apple-system, sans-serif;
                pointer-events: auto; /* 確保可以點擊 */
                cursor: default;
            }
            .mvf-overlay:hover {
                opacity: ${CONFIG.ui.opacityHover};
            }
            /* 針對父元素 hover 偵測,實現更寬鬆的觸發區域 */
            .mvf-parent-hover .mvf-overlay {
                opacity: ${CONFIG.ui.opacityHover};
            }
            .mvf-slider {
                -webkit-appearance: none;
                width: 80px; /* 寬度稍微放寬 */
                height: 4px;
                background: rgba(255, 255, 255, 0.3);
                border-radius: 2px;
                outline: none;
                cursor: pointer;
            }
            .mvf-slider::-webkit-slider-thumb {
                -webkit-appearance: none;
                appearance: none;
                width: 12px;
                height: 12px;
                border-radius: 50%;
                background: #fff;
                cursor: pointer;
                transition: transform 0.1s;
            }
            .mvf-slider::-webkit-slider-thumb:hover {
                transform: scale(1.2);
            }
            .mvf-text {
                color: ${CONFIG.ui.color};
                font-size: 12px;
                font-weight: bold;
                min-width: 32px;
                text-align: right;
                user-select: none;
            }
        `;

        const style = document.createElement('style');
        style.id = styleId;
        style.textContent = css;
        document.head.appendChild(style);
    }

    /**
     * 創建音量控制 UI
     * @param {HTMLVideoElement} video
     */
    function createVolumeUI(video) {
        const parent = video.parentNode;
        // 查找是否已經有 UI (避免重複添加)
        let container = parent.querySelector('.mvf-overlay');
        
        // 如果 UI 不存在,則建立
        if (!container) {
            // 確保父層容器有定位屬性
            const parentStyle = window.getComputedStyle(parent);
            if (parentStyle.position === 'static') {
                parent.style.position = 'relative';
            }

            container = document.createElement('div');
            container.className = 'mvf-overlay';
            
            // ==========================================================
            // V4.4 修復: 移除事件捕獲階段的阻擋,改為只在冒泡階段停止傳播
            // 這樣可以確保滑桿可以接收到 MOUSEDOWN 事件,但不會傳遞到下方的影片。
            // ==========================================================
            const stopPropagation = (e) => {
                e.stopPropagation();
            };
            
            // 只在冒泡階段停止傳播 (不會阻止滑桿本身的互動)
            container.addEventListener('click', stopPropagation);
            container.addEventListener('mousedown', stopPropagation);
            container.addEventListener('touchstart', stopPropagation);
            container.addEventListener('dblclick', stopPropagation);
            // ==========================================================


            const slider = document.createElement('input');
            slider.type = 'range';
            slider.className = 'mvf-slider';
            slider.min = '0';
            slider.max = '1';
            slider.step = '0.01';
            
            const text = document.createElement('span');
            text.className = 'mvf-text';

            container.appendChild(slider);
            container.appendChild(text);
            parent.appendChild(container);

            // 綁定滑桿事件
            slider.addEventListener('input', (e) => {
                const val = parseFloat(e.target.value);
                video.volume = val;
                text.textContent = Math.round(val * 100) + '%';
                
                // 標記為使用者手動設定 (用於防爆音邏輯)
                video.dataset.userManualSet = 'true';
                video.dataset.userMaxVolume = (val === 1) ? 'true' : 'false';
            });
            
            // 處理懸浮顯示
            parent.addEventListener('mouseenter', () => container.style.opacity = CONFIG.ui.opacityHover);
            parent.addEventListener('mouseleave', () => container.style.opacity = CONFIG.ui.opacityIdle);
        }

        // 更新 UI 狀態以符合影片當前音量 (無論是新建立還是既有的)
        const slider = container.querySelector('.mvf-slider');
        const text = container.querySelector('.mvf-text');
        
        if (slider && text) {
            slider.value = video.volume;
            text.textContent = Math.round(video.volume * 100) + '%';
        }
    }

    /**
     * 設定單個影片元素的音量與 UI
     * @param {HTMLVideoElement} videoElement
     */
    function adjustVolume(videoElement) {
        // 1. 初始化設定 (初次發現或重複使用時)
        if (!videoElement.dataset.mvfInitialized) {
            videoElement.dataset.mvfInitialized = 'true';
            
            // 初始音量設定
            videoElement.volume = CONFIG.defaultVolume;
            videoElement.dataset.volumeAdjusted = 'true';
            
            // 監聽:音量變化 (同步 UI + 防爆音)
            videoElement.addEventListener('volumechange', () => {
                // 同步 UI
                const parent = videoElement.parentNode;
                const slider = parent.querySelector('.mvf-slider');
                const text = parent.querySelector('.mvf-text');
                if (slider && text) {
                    slider.value = videoElement.volume;
                    text.textContent = Math.round(videoElement.volume * 100) + '%';
                }

                // 防爆音邏輯
                // 檢查是否為 100% 且不是使用者手動設為 100%
                if (videoElement.volume === 1 && videoElement.dataset.userMaxVolume !== 'true') {
                    // 如果有手動設定過非 100% 的值,則嘗試恢復到該值 (此處為 V2.2 簡化邏輯)
                    videoElement.volume = CONFIG.defaultVolume; 
                    log('攔截到網站嘗試將音量重置為 100%,已駁回,恢復為預設 10%。');
                }
            });

            // 監聽:影片來源載入 (關鍵:處理動態牆影片回收機制)
            videoElement.addEventListener('loadstart', () => {
                log('偵測到影片來源變更 (Recycle/Loadstart),重置狀態。');
                
                // 重置所有使用者手動標記
                videoElement.dataset.userManualSet = 'false';
                videoElement.dataset.userMaxVolume = 'false';
                
                // 強制將音量設回預設值
                setTimeout(() => {
                    videoElement.volume = CONFIG.defaultVolume;
                    createVolumeUI(videoElement); // 確保 UI 數值同步
                }, 0);
            });
        }

        // 2. 確保 UI 存在並更新
        createVolumeUI(videoElement);
    }
    
    /**
     * V4.5 增強: 週期性掃描 DOM 中所有未初始化的影片
     */
    function scanForUninitializedVideos() {
        document.querySelectorAll('video').forEach(videoElement => {
            // 檢查 data-mvfInitialized 標記
            if (!videoElement.dataset.mvfInitialized) {
                log('週期性掃描發現未初始化影片,正在處理...');
                adjustVolume(videoElement);
            }
        });
    }


    /**
     * 處理頁面上現有的和未來新增的影片
     */
    function observePage() {
        injectStyles();

        // 1. 建立 MutationObserver (處理持續新增的內容)
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeName === 'VIDEO') {
                        adjustVolume(node);
                    } else if (node.nodeType === 1) {
                        const videos = node.querySelectorAll('video');
                        videos.forEach(adjustVolume);
                    }
                });
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 2. 處理現有影片 (確保頁面載入時的內容被處理)
        document.querySelectorAll('video').forEach(adjustVolume);
        
        // 3. V4.5 增強: 設置週期性檢查 (每 3 秒),處理可能被 MutationObserver 遺漏的影片
        setInterval(scanForUninitializedVideos, 3000); 
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', observePage);
    } else {
        observePage();
    }

})();