Greasy Fork

Greasy Fork is available in English.

Meta reel 音量控制

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

当前为 2025-11-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴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  V4.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();
    }

})();