Greasy Fork

Greasy Fork is available in English.

Meta 社交媒体音量控制大师 (FB, IG, Threads) - UI整合版

基于 v2.2 核心逻辑,将音量滑块精确地整合到视频**底部控制栏** (位于原生控制项左侧的独立空间)。专注于默认音量 10% 和防爆音。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Meta 社交媒體音量控制大師 (FB, IG, Threads) - UI整合版
// @name:zh-TW   Meta 社交媒體音量控制大師 (FB, IG, Threads) - UI整合版
// @name:zh-CN   Meta 社交媒体音量控制大师 (FB, IG, Threads) - UI整合版
// @name:en      Meta Media Volume Master (FB, IG, Threads) - UI Integrated
// @name:ja      Meta メディア音量マスター (FB, IG, Threads) - UI統合版
// @namespace    http://tampermonkey.net/
// @version      4.1
// @description  基於 v2.2 核心邏輯,將音量滑桿精確地整合到影片**底部控制列** (位於原生控制項左側的獨立空間)。專注於預設音量 10% 和防爆音。
// @description:zh-TW 基於 v2.2 核心邏輯,將音量滑桿精確地整合到影片**底部控制列** (位於原生控制項左側的獨立空間)。專注於預設音量 10% 和防爆音。
// @description:zh-CN 基于 v2.2 核心逻辑,将音量滑块精确地整合到视频**底部控制栏** (位于原生控制项左侧的独立空间)。专注于默认音量 10% 和防爆音。
// @description:en Based on v2.2 core logic, the volume slider is precisely integrated into the video's **bottom control bar** (positioned in independent space to the left of native controls). Focuses on default 10% volume and anti-blast function.
// @description:ja v2.2のコアロジックに基づき、音量スライダーを動画の**下部コントロールバー**(ネイティブコントロールの左側の独立したスペース)に正確に統合。デフォルト音量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: 1.0,   // 平常的透明度 (常駐顯示)
            opacityHover: 1.0,  // 滑鼠移上去的透明度
            positionBottom: '8px', // V4.1: 底部位置,位於控制列高度中央
            positionRight: '120px',  // V4.1: 大幅向左移動,避開原生按鈕,位於獨立空間
            color: '#ffffff',   // 文字與圖示顏色
            bgColor: 'transparent' // 背景顏色設為透明
        }
    };
    // ==========================================================

    function log(msg) {
        if (CONFIG.showLogs) {
            console.log(`[音量控制 v4.1 - v2.2核心] ${msg}`);
        }
    }

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

        const css = `
            .mvf-overlay {
                position: absolute;
                bottom: ${CONFIG.ui.positionBottom}; /* 定位到底部,位於控制列高度中央 */
                right: ${CONFIG.ui.positionRight};   /* 避開原生右側按鈕的獨立空間 */
                z-index: 2147483647; /* 提升到最高層級,確保在原生 UI 之上 */
                background-color: ${CONFIG.ui.bgColor}; /* 透明背景 */
                display: flex;
                align-items: center;
                gap: 4px; /* 縮小圖示和文字間距 */
                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};
            }
            .mvf-slider {
                -webkit-appearance: none;
                width: 60px; /* 縮小滑桿寬度 */
                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: 10px; /* 縮小字體大小 */
                font-weight: bold;
                min-width: 24px; /* 縮小最小寬度 */
                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.2 核心邏輯)
            container.addEventListener('click', (e) => e.stopPropagation());
            container.addEventListener('dblclick', (e) => e.stopPropagation());
            container.addEventListener('mousedown', (e) => e.stopPropagation());
            container.addEventListener('touchstart', (e) => e.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';
            });
        }

        // 更新 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) + '%';
                }

                // 防爆音邏輯
                if (videoElement.volume === 1 && videoElement.dataset.userMaxVolume !== 'true') {
                    videoElement.volume = CONFIG.defaultVolume;
                    log('攔截到網站嘗試將音量重置為 100%,已駁回。');
                }
            });

            // 監聽:影片來源載入 (關鍵:處理動態牆影片回收機制)
            videoElement.addEventListener('loadstart', () => {
                log('偵測到影片來源變更 (Recycle/Loadstart),重置狀態。');
                
                // 重置所有使用者手動標記,因為這是一部新影片
                videoElement.dataset.userManualSet = 'false';
                videoElement.dataset.userMaxVolume = 'false';
                
                // 強制將音量設回預設值
                // 使用 setTimeout 確保在瀏覽器預設行為之後執行
                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();
    }

})();