// ==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 // @downloadURL https://update.greasyfork.icu/scripts/556533/Meta%20reel%20%E9%9F%B3%E9%87%8F%E6%8E%A7%E5%88%B6.user.js // @updateURL https://update.greasyfork.icu/scripts/556533/Meta%20reel%20%E9%9F%B3%E9%87%8F%E6%8E%A7%E5%88%B6.meta.js // ==/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(); } })();