// ==UserScript==
// @name Facebook 自動展開與互動增強
// @namespace http://tampermonkey.net/
// @version 2025.05.14.12
// @description 混合觸發模式:滑鼠游標自動展開查看更多 + 點讚 + 影片音量調整 + 左下角控制面板
// @author You
// @match https://www.facebook.com/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
// 常數與狀態初始化
const CLICK_INTERVAL = 500;
const HIDDEN_STYLE_SELECTOR = '.xwib8y2.x1y1aw1k.xwya9rg.x1n2onr6';
const state = {
lastClickTime: 0,
likeCoolingDown: false,
panelCollapsed: false,
DEFAULT_VOLUME: GM_getValue('DEFAULT_VOLUME', 0.2),
COLUMN_COUNT: GM_getValue('COLUMN_COUNT', 3),
buttons: {
like: GM_getValue('likeEnabled', true),
seeMore: GM_getValue('seeMoreEnabled', true),
otherExpand: GM_getValue('otherExpandEnabled', true),
volume: GM_getValue('volumeEnabled', true),
columns: GM_getValue('columnsEnabled', false),
hideStyle: GM_getValue('hideStyleEnabled', false),
wideLayout: GM_getValue('wideLayoutEnabled', false)
}
};
// 效能優化:快取常用元素
let cachedElements = {
panel: null,
videoElements: null,
lastVideoCheck: 0
};
// 控制面板創建
function createControlPanel() {
const panel = document.createElement('div');
Object.assign(panel.style, {
position: 'fixed',
left: '0px',
bottom: '30px',
zIndex: '9999',
display: 'flex',
flexDirection: 'column',
gap: '5px',
backgroundColor: 'transparent',
padding: '10px',
borderRadius: '8px'
});
try {
// 按鈕創建函數
const createButton = (text, key, action) => {
const btn = document.createElement('button');
Object.assign(btn.style, {
padding: '8px 12px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold',
width: '40px',
textAlign: 'center'
});
btn.innerText = text;
btn.addEventListener('click', () => {
state.buttons[key] = !state.buttons[key];
GM_setValue(`${key}Enabled`, state.buttons[key]);
updateButtonStyle(btn, state.buttons[key]);
action && action();
});
updateButtonStyle(btn, state.buttons[key]);
return btn;
};
// 創建主要功能按鈕
panel.append(
createButton('讚', 'like'),
createButton('看', 'seeMore'),
createButton('回', 'otherExpand'),
createButton('音', 'volume', () => state.buttons.volume && processAllVideos())
);
// 音量控制組(固定黑色背景白字樣式)
const volumeControlGroup = createControlGroup([
createSmallButton('-', () => adjustVolume(-0.1)),
createSmallButton('+', () => adjustVolume(0.1))
]);
panel.append(volumeControlGroup);
// 欄位控制(如果可用)
if (hasColumnCountCSS()) {
panel.append(
createButton('欄', 'columns', () => state.buttons.columns && applyColumnCount()),
createControlGroup([
createSmallButton('-', () => adjustColumnCount(-1)),
createSmallButton('+', () => adjustColumnCount(1))
])
);
}
// 其他功能按鈕
panel.append(
createButton('動', 'hideStyle', toggleStyleVisibility)
);
// 寬版佈局按鈕(如果可用)
if (hasWideLayoutCSS()) {
panel.append(
createButton('放', 'wideLayout', toggleWideLayout)
);
}
// 折疊按鈕(固定黑色背景白字樣式)
const collapseBtn = document.createElement('button');
Object.assign(collapseBtn.style, {
padding: '8px 12px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold',
width: '40px',
textAlign: 'center',
backgroundColor: '#000000',
color: '#FFFFFF'
});
collapseBtn.innerText = state.panelCollapsed ? 'Δ' : '∇';
collapseBtn.addEventListener('click', () => {
state.panelCollapsed = !state.panelCollapsed;
GM_setValue('panelCollapsed', state.panelCollapsed);
collapseBtn.innerText = state.panelCollapsed ? 'Δ' : '∇';
togglePanelCollapse();
});
panel.append(collapseBtn);
document.body.appendChild(panel);
cachedElements.panel = panel;
state.panelCollapsed && togglePanelCollapse();
} catch {}
}
// 輔助函數
function createControlGroup(buttons) {
const group = document.createElement('div');
Object.assign(group.style, {
display: 'flex',
justifyContent: 'space-between',
width: '40px',
marginTop: '-5px'
});
buttons && buttons.forEach(btn => group.append(btn));
return group;
}
function createSmallButton(text, action) {
const btn = document.createElement('button');
Object.assign(btn.style, {
padding: '2px 0',
border: '1px solid #000000',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
width: '20px',
textAlign: 'center',
backgroundColor: '#000000',
color: '#FFFFFF'
});
btn.innerText = text;
btn.addEventListener('click', action);
return btn;
}
function updateButtonStyle(btn, isActive) {
Object.assign(btn.style, {
backgroundColor: isActive ? '#1877f2' : '#e4e6eb',
color: isActive ? 'white' : '#65676b'
});
}
// 面板折疊功能
function togglePanelCollapse() {
const buttons = cachedElements.panel.querySelectorAll('button');
buttons.forEach(btn => {
if (!['Δ', '∇', '+', '-'].includes(btn.innerText)) {
btn.style.display = state.panelCollapsed ? 'none' : 'block';
}
});
}
// 寬版佈局檢測與切換
function hasWideLayoutCSS() {
return Array.from(document.styleSheets).some(sheet => {
try {
return Array.from(sheet.cssRules).some(rule =>
rule.media && rule.media.mediaText.includes('min-width: 1900px')
);
} catch {}
});
}
function toggleWideLayout() {
Array.from(document.styleSheets).forEach(sheet => {
try {
Array.from(sheet.cssRules).forEach(rule => {
if (rule.media && (rule.media.mediaText.includes('min-width: 1900px') ||
rule.media.mediaText.includes('min-width: 9999px'))) {
rule.media.mediaText = state.buttons.wideLayout ?
rule.media.mediaText.replace('9999px', '1900px') :
rule.media.mediaText.replace('1900px', '9999px');
}
});
} catch {}
});
}
// 樣式切換功能
function toggleStyleVisibility() {
document.querySelectorAll(HIDDEN_STYLE_SELECTOR).forEach(el => {
el.style.display = state.buttons.hideStyle ? 'none' : '';
});
}
// 欄位佈局功能
function hasColumnCountCSS() {
return getComputedStyle(document.documentElement).getPropertyValue('--column-count').trim() !== '';
}
function applyColumnCount() {
if (state.buttons.columns) {
document.documentElement.style.setProperty('--column-count', state.COLUMN_COUNT);
}
}
function adjustColumnCount(change) {
state.COLUMN_COUNT = Math.max(1, state.COLUMN_COUNT + change);
GM_setValue('COLUMN_COUNT', state.COLUMN_COUNT);
state.buttons.columns && applyColumnCount();
}
// 音量控制
function adjustVolume(change) {
state.DEFAULT_VOLUME = Math.min(1, Math.max(0, state.DEFAULT_VOLUME + change));
GM_setValue('DEFAULT_VOLUME', state.DEFAULT_VOLUME);
state.buttons.volume && processAllVideos();
}
function processAllVideos() {
const now = Date.now();
// 每5秒才重新查詢一次video元素
if (now - cachedElements.lastVideoCheck > 5000) {
cachedElements.videoElements = document.querySelectorAll('video');
cachedElements.lastVideoCheck = now;
}
cachedElements.videoElements && cachedElements.videoElements.forEach(video => {
try {
if (typeof video.volume === 'number') {
video.volume = state.DEFAULT_VOLUME;
video.muted = false;
}
} catch {}
});
}
// 自動互動功能
const debouncedHandleOtherButtons = debounce(handleOtherButtons, 300);
document.addEventListener('mouseover', function(event) {
const target = event.target;
if (state.buttons.seeMore && isSeeMoreButton(target) && checkClickInterval()) {
safeClick(target);
}
});
function handleOtherButtons() {
if (!state.buttons.otherExpand) return;
document.querySelectorAll('div[role="button"]:not([aria-expanded="true"])').forEach(btn => {
if (isOtherExpandButton(btn) && checkClickInterval()) {
safeClick(btn);
}
});
}
document.addEventListener('mouseover', function(event) {
if (!state.buttons.like || state.likeCoolingDown) return;
const target = event.target.closest('div[aria-label="讚"]');
if (target && target.getAttribute('aria-pressed') !== 'true' && isButtonVisible(target)) {
state.likeCoolingDown = true;
setTimeout(() => { state.likeCoolingDown = false; }, 1000);
safeClick(target);
}
});
// 工具函數
function isSeeMoreButton(element) {
return element?.getAttribute?.('role') === 'button' &&
element.getAttribute('aria-expanded') !== 'true' &&
element.textContent.trim() === '查看更多';
}
function isOtherExpandButton(element) {
const text = element?.textContent?.trim();
return text && text !== '查看更多' &&
(/^查看全部\d+則回覆$/.test(text) ||
/.+已回覆/.test(text) ||
/^查看 \d+ 則回覆$/.test(text));
}
function checkClickInterval() {
const now = Date.now();
if (now - state.lastClickTime > CLICK_INTERVAL) {
state.lastClickTime = now;
return true;
}
return false;
}
function safeClick(element) {
element?.click?.();
}
function isButtonVisible(button) {
if (!button) return false;
const rect = button.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 &&
rect.top >= 0 && rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
}
function debounce(func, wait) {
let timeout;
return function() {
const context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// 初始化
function init() {
createControlPanel();
(state.buttons.seeMore || state.buttons.otherExpand) && setInterval(debouncedHandleOtherButtons, 800);
const observer = new MutationObserver(() => {
state.buttons.seeMore && state.buttons.otherExpand && handleOtherButtons();
state.buttons.volume && processAllVideos();
state.buttons.columns && applyColumnCount();
state.buttons.hideStyle && toggleStyleVisibility();
state.buttons.wideLayout !== undefined && toggleWideLayout();
});
observer.observe(document.body, { childList: true, subtree: true });
// 初始狀態應用
state.buttons.volume && processAllVideos();
state.buttons.columns && applyColumnCount();
state.buttons.hideStyle && toggleStyleVisibility();
state.buttons.wideLayout !== undefined && toggleWideLayout();
}
// 啟動腳本
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
})();