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