Greasy Fork

Greasy Fork is available in English.

Meta 社交媒体音量控制大师 (FB, IG, Threads) - 右上角稳定版

回归到右上角悬浮 UI (鼠标移入时显示),采用 v2.9 的最强事件阻挡策略,彻底解决点击 UI 导致视频暂停的问题。专注于默认音量 10% 和防爆音。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Meta 社交媒體音量控制大師 (FB, IG, Threads) - 右上角穩定版
// @name:zh-TW   Meta 社交媒體音量控制大師 (FB, IG, Threads) - 右上角穩定版
// @name:zh-CN   Meta 社交媒体音量控制大师 (FB, IG, Threads) - 右上角稳定版
// @name:en      Meta Media Volume Master (FB, IG, Threads) - Top-Right Stable
// @name:ja      Meta メディア音量マスター (FB, IG, Threads) - 右上安定版
// @namespace    http://tampermonkey.net/
// @version      4.2
// @description  回歸到右上角懸浮 UI (滑鼠移入時顯示),採用 v2.9 的最強事件阻擋策略,徹底解決點擊 UI 導致影片暫停的問題。專注於預設音量 10% 和防爆音。
// @description:zh-TW 回歸到右上角懸浮 UI (滑鼠移入時顯示),採用 v2.9 的最強事件阻擋策略,徹底解決點擊 UI 導致影片暫停的問題。專注於預設音量 10% 和防爆音。
// @description:zh-CN 回归到右上角悬浮 UI (鼠标移入时显示),采用 v2.9 的最强事件阻挡策略,彻底解决点击 UI 导致视频暂停的问题。专注于默认音量 10% 和防爆音。
// @description:en Reverts to the top-right floating UI (visible on hover) using the strongest v2.9 event blocking strategy to permanently fix accidental video pausing. Focuses on default 10% volume and anti-blast function.
// @description:ja 右上隅のフローティングUI(ホバー時表示)に戻し、v2.9の最強イベントブロック戦略を採用して、UIクリックによる誤操作を完全に防止。デフォルト音量10%と爆音防止に焦点を当てています。
// @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.1,   // V4.2: 平常的透明度 (懸浮隱藏)
            opacityHover: 1.0,  // V4.2: 滑鼠移上去的透明度
            positionTop: '10px', // V4.2: 右上角定位
            positionRight: '10px',  // V4.2: 右上角定位
            color: '#ffffff',   // 文字與圖示顏色
            bgColor: 'rgba(0, 0, 0, 0.4)' // 背景顏色設為半透明黑色
        }
    };
    // ==========================================================

    function log(msg) {
        if (CONFIG.showLogs) {
            console.log(`[音量控制 v4.2 - 右上角穩定版] ${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;
                /* 為了防止父元素覆蓋,我們在父元素上也添加 hover 效果 */
                /* 這是為了讓滑鼠在 UI 附近時就能顯示 */
            }
            .mvf-overlay:hover {
                opacity: ${CONFIG.ui.opacityHover};
            }
            /* 針對父元素 hover 偵測,實現更寬鬆的觸發區域 */
            /* 確保滑鼠在影片上任何地方都有效,但在實際 UI 上才操作 */
            .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';
            
            // ==========================================================
            // V2.9 - 最強事件阻擋策略 (事件捕獲階段終止事件)
            // ==========================================================
            // 由於事件穿透問題難解,我們在事件捕獲階段就終止所有可能導致暫停的事件
            const stopAndPrevent = (e) => {
                e.stopPropagation();
                e.preventDefault();
            };
            
            // 捕獲階段監聽 (true)
            container.addEventListener('click', stopAndPrevent, true);
            container.addEventListener('mousedown', stopAndPrevent, true);
            container.addEventListener('touchstart', stopAndPrevent, true);
            // ==========================================================


            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);
    }

    /**
     * 處理頁面上現有的和未來新增的影片
     */
    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);
    }

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

})();