// ==UserScript==
// @name Facebook 自動展開與互動增強
// @namespace http://tampermonkey.net/
// @version 2025.05.18.13
// @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 EXPANDED_CONTAINER_SELECTOR = '.xh8yej3.xrgej4m.xcatxm7.xryxfnj.x1plvlek.x78zum5.xfkwgsy.x1mh14rs.x58fqnu.xrjkcco.x1qjc9v5.x71s49j.x1a2a7pz.xdt5ytf.x1afcbsf.x1ja2u2z.x1n2onr6';
const state = {
lastClickTime: 0,
likeCoolingDown: false,
panelCollapsed: GM_getValue('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
};
let observer = null;
let eventListeners = [];
function cleanup() {
observer?.disconnect();
eventListeners.forEach(({element, type, handler}) => {
element?.removeEventListener?.(type, handler);
});
cachedElements.panel?.remove();
cachedElements = null;
}
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'
});
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;
const handler = () => {
state.buttons[key] = !state.buttons[key];
GM_setValue(`${key}Enabled`, state.buttons[key]);
updateButtonStyle(btn, state.buttons[key]);
action?.();
};
btn.addEventListener('click', handler);
eventListeners.push({element: btn, type: 'click', handler});
updateButtonStyle(btn, state.buttons[key]);
return btn;
};
const buttons = [
createButton('讚', 'like'),
createButton('看', 'seeMore'),
createButton('回', 'otherExpand'),
createButton('音', 'volume', () => state.buttons.volume && processAllVideos())
];
const volumeControlGroup = createControlGroup([
createSmallButton('-', () => adjustVolume(-0.1)),
createSmallButton('+', () => adjustVolume(0.1))
]);
buttons.push(volumeControlGroup);
if (hasColumnCountCSS()) {
buttons.push(
createButton('欄', 'columns', () => state.buttons.columns && applyColumnCount()),
createControlGroup([
createSmallButton('-', () => adjustColumnCount(-1)),
createSmallButton('+', () => adjustColumnCount(1))
])
);
}
buttons.push(createButton('動', 'hideStyle', toggleStyleVisibility));
if (hasWideLayoutCSS()) {
buttons.push(createButton('放', 'wideLayout', toggleWideLayout));
}
const collapseBtn = createCollapseButton();
buttons.push(collapseBtn);
buttons.forEach(btn => panel.appendChild(btn));
document.body.appendChild(panel);
cachedElements.panel = panel;
state.panelCollapsed && togglePanelCollapse();
}
function createCollapseButton() {
const btn = document.createElement('button');
Object.assign(btn.style, {
padding: '8px 12px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold',
width: '40px',
textAlign: 'center',
backgroundColor: '#000000',
color: '#FFFFFF'
});
btn.innerText = state.panelCollapsed ? 'Δ' : '∇';
const handler = () => {
state.panelCollapsed = !state.panelCollapsed;
GM_setValue('panelCollapsed', state.panelCollapsed);
btn.innerText = state.panelCollapsed ? 'Δ' : '∇';
togglePanelCollapse();
};
btn.addEventListener('click', handler);
eventListeners.push({element: btn, type: 'click', handler});
return btn;
}
function createControlGroup(buttons) {
const group = document.createElement('div');
Object.assign(group.style, {
display: 'flex',
justifyContent: 'space-between',
width: '40px',
marginTop: '-5px'
});
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;
const handler = () => action();
btn.addEventListener('click', handler);
eventListeners.push({element: btn, type: 'click', handler});
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?.mediaText.includes('min-width: 1900px')
);
} catch { return false; }
});
}
function toggleWideLayout() {
Array.from(document.styleSheets).forEach(sheet => {
try {
Array.from(sheet.cssRules).forEach(rule => {
if (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() {
const elements = document.querySelectorAll(HIDDEN_STYLE_SELECTOR);
elements.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();
if (now - cachedElements.lastVideoCheck > 5000) {
cachedElements.videoElements = document.querySelectorAll('video');
cachedElements.lastVideoCheck = now;
}
cachedElements.videoElements?.forEach(video => {
try {
if (typeof video.volume === 'number') {
video.volume = state.DEFAULT_VOLUME;
video.muted = false;
}
} catch {}
});
}
function isContainerExpanded(button) {
const container = button.closest(EXPANDED_CONTAINER_SELECTOR);
return container && getComputedStyle(container).display !== 'none';
}
const throttledHandleMouseOver = throttle(handleMouseOver, 200);
const debouncedHandleOtherButtons = debounce(handleOtherButtons, 300);
function handleMouseOver(event) {
const target = event.target;
if (state.buttons.seeMore && isSeeMoreButton(target) && checkClickInterval()) {
safeClick(target);
}
if (state.buttons.like && !state.likeCoolingDown) {
const likeButton = target.closest('div[aria-label="讚"]');
if (likeButton && likeButton.getAttribute('aria-pressed') !== 'true' && isButtonVisible(likeButton)) {
state.likeCoolingDown = true;
setTimeout(() => { state.likeCoolingDown = false; }, 1000);
safeClick(likeButton);
}
}
}
function handleOtherButtons() {
if (!state.buttons.otherExpand) return;
document.querySelectorAll('div[role="button"]:not([aria-expanded="true"])').forEach(btn => {
if (isOtherExpandButton(btn) && checkClickInterval()) {
safeClick(btn);
}
});
}
function isSeeMoreButton(element) {
return element?.getAttribute?.('role') === 'button' &&
element.getAttribute('aria-expanded') !== 'true' &&
element.textContent.trim() === '查看更多';
}
function isOtherExpandButton(element) {
if (!element?.getAttribute || element.getAttribute('aria-expanded') === 'true') {
return false;
}
const text = element.textContent.trim();
const isReplyButton = text && text !== '查看更多' &&
(/^查看全部\d+則回覆$/.test(text) ||
/.+已回覆/.test(text) ||
/^查看 \d+ 則回覆$/.test(text));
return isReplyButton && isContainerExpanded(element);
}
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 throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
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();
if (state.buttons.seeMore || state.buttons.otherExpand) {
const intervalId = setInterval(debouncedHandleOtherButtons, 800);
eventListeners.push({element: window, type: 'interval', handler: intervalId});
}
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 });
const mouseOverHandler = throttledHandleMouseOver;
document.addEventListener('mouseover', mouseOverHandler);
eventListeners.push({element: document, type: 'mouseover', handler: mouseOverHandler});
window.addEventListener('unload', cleanup);
}
if (document.readyState === 'complete') {
init();
} else {
const loadHandler = () => {
init();
window.removeEventListener('load', loadHandler);
};
window.addEventListener('load', loadHandler);
}
})();