No longer reliant on Stylus, bundled and unified—I am complete. Multi-column layout + auto-expand "See More" on hover, auto-like, volume control, and comment expansion. (Expanded trigger range +20px)
// ==UserScript==
// @name Facebook Worker's Wisdom DX
// @name:zh-TW Facebook 工人智慧 DX
// @namespace http://tampermonkey.net/
// @version 2026.04.18.5
// @description:zh-TW 不再需要stylus樣式表,打包合併,我即為全。多欄化樣式、滑鼠游標自動展開「查看更多」、點讚、音量調整、留言展開。(觸發範圍+20px)
// @description No longer reliant on Stylus, bundled and unified—I am complete. Multi-column layout + auto-expand "See More" on hover, auto-like, volume control, and comment expansion. (Expanded trigger range +20px)
// @author Dxzy
// @match https://www.facebook.com/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// = ================= 配置設定 / CONFIGURATION =================
const CONFIG = {
ALLOWED_PATHS: [
/^\/$/,
/^\/\?filter=(all|favorites|friends|groups|pages)&sk=h_chr/,
/^\/[^/]+\/posts\/.*$/,
/^\/reel\/.*$/,
/^\/search\/.*$/,
/^\/photo.*$/,
/^\/profile\.php.*$/,
/^\/[^/]+\/videos\/.*$/,
/^\/groups\/\d+(?:\/.*)?$/,
/^\/permalink\.php\?.*$/,
/^\/watch\/.*$/
],
AUTO_EXPAND_SELECTOR: '.x1qjc9v5.x71s49j.x1a2a7pz .xeuugli.xbelrpt',
SEE_MORE_SELECTOR: '.x6o7n8i .x1lliihq .x126k92a .xzsf02u.x1i10hfl',
POST_LIKE_SELECTOR: '.x5ve5x3 > .x9f619',
COMMENT_LIKE_SELECTOR: '.x1rg5ohu.x1ypdohk.xi81zsa',
LIKE_SPAN_SELECTOR: 'span.x1rg5ohu.xxymvpz',
POST_LIKE_ICON_SELECTOR: 'i[data-visualcompletion="css-img"]',
POST_DATA_STORE_ID: '[data-store-id]',
UNLIKED_COMMENT_CLASS: 'x1fiuzfb',
LIKED_POST_CLASS: 'xq8hly8',
UNLIKED_POST_CLASS: 'x1d69dk1',
CLICK_INTERVAL: 300,
CHECK_INTERVAL: 800,
LIKE_COOLDOWN: 1000,
THROTTLE_MS: 100,
DEBOUNCE_MS: 500,
OBSERVER_DEBOUNCE_MS: 300,
NAVIGATION_CHECK_MS: 2000,
DEFAULT_VOLUME: 0.2,
COLUMN_COUNT: 4,
COLUMN_GAP: 15,
TRIGGER_EXPAND_PX: 20
};
// = ================= 狀態管理 / STATE MANAGEMENT =================
const state = {
lastClickTime: 0,
likeCoolingDown: false,
pendingLikeTarget: null,
panelCollapsed: GM_getValue('panelCollapsed', false),
buttons: {
like: GM_getValue('likeEnabled', false),
otherExpand: GM_getValue('otherExpandEnabled', false),
volume: GM_getValue('volumeEnabled', false),
columns: GM_getValue('columnsEnabled', false)
},
settings: {
volume: GM_getValue('DEFAULT_VOLUME', CONFIG.DEFAULT_VOLUME),
columns: GM_getValue('COLUMN_COUNT', CONFIG.COLUMN_COUNT)
},
isActivePage: false,
lastObserverRun: 0,
panelCreated: false
};
// = ================= 資源管理 / RESOURCE MANAGEMENT =================
const resources = {
styleTag: null,
observers: { dom: null, video: null },
intervals: { periodicCheck: null, navCheck: null },
panel: null,
eventHandlers: [],
videoElements: []
};
// = 防重複機制 / Deduplication
const autoClickedLikeIds = new Set();
const recentlyClickedElements = new WeakSet();
// = ================= 工具函數 (外置提升性能) / UTILITIES =================
function throttle(func, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function isWithinExpandedRange(clientX, clientY, rect, px) {
return clientX >= rect.left - px && clientX <= rect.right + px &&
clientY >= rect.top - px && clientY <= rect.bottom + px;
}
function safeClick(el) { el?.isConnected && el.click(); }
function isButtonVisible(btn) {
if (!btn) return false;
const r = btn.getBoundingClientRect();
return r.width > 5 && r.height > 5 &&
r.top < window.innerHeight + 100 && r.bottom > -100 &&
r.left < window.innerWidth + 100 && r.right > -100;
}
function isAllowedPage() {
try {
const path = window.location.pathname + window.location.search;
return CONFIG.ALLOWED_PATHS.some(p => p.test(path));
} catch { return false; }
}
// = ================= 資源清理 / RESOURCE CLEANUP =================
function cleanupResources() {
resources.eventHandlers.forEach(({element, type, handler, options}) => {
try { element?.removeEventListener?.(type, handler, options); } catch {}
});
resources.eventHandlers = [];
Object.values(resources.observers).forEach(obs => obs?.disconnect?.());
resources.observers.dom = resources.observers.video = null;
Object.values(resources.intervals).forEach(id => id && clearInterval(id));
resources.intervals.periodicCheck = resources.intervals.navCheck = null;
removeMultiColumnCSS();
if (resources.panel?.isConnected) resources.panel.remove();
resources.panel = null;
state.panelCreated = false;
}
// = ================= 多欄樣式管理 / MULTI-COLUMN STYLE =================
function injectMultiColumnCSS() {
if (!state.isActivePage || !state.buttons.columns) { removeMultiColumnCSS(); return; }
removeMultiColumnCSS();
const style = document.createElement('style');
style.id = 'fb-worker-wisdom-multicolumn';
style.textContent = generateMultiColumnCSS();
document.head.appendChild(style);
resources.styleTag = style;
}
function removeMultiColumnCSS() {
if (resources.styleTag?.isConnected) resources.styleTag.remove();
const existing = document.getElementById('fb-worker-wisdom-multicolumn');
if (existing?.isConnected) existing.remove();
resources.styleTag = null;
}
function generateMultiColumnCSS() {
return `
:root { --column-count: ${state.settings.columns}; --column-gap: ${CONFIG.COLUMN_GAP}px; --max-post-height: 80vh; --sidebar-width: 60px; }
.x1v0nzow.x1ceravr.x17zi3g0.xvue9z.x193iq5w, .x1xwk8fm.x193iq5w, .xsfy40s.x1miatn0.x9f619 { width: calc(110% - var(--sidebar-width)); margin-right: -60px; }
.xornbnt.x1t2pt76.xylbxtu.x1q0g3np.xozqiw3.x1cy8zhl.x1qughib.xeuugli.xs83m0k.x1iyjqo2.x1r8uery.x1n2onr6.x78zum5.x1ja2u2z.x9f619, .x1t2pt76.x78zum5.xs83m0k.x1iyjqo2.x1r8uery.xeuugli.x193iq5w.xdt5ytf.x1ja2u2z.x1n2onr6.x9f619 { max-width: 100%; }
.xxzkxad.x9e5oc1, .xh78kpn.xcoz2nd.x2bj2ny, .x1vjfegm.x2lah0s.xeuugli { max-width: var(--sidebar-width); min-width: var(--sidebar-width); position: absolute; z-index: 1; }
.x6o7n8i.x1unhpq9.x1hc1fzr > div > div, .x1xwk8fm.x193iq5w { display: flex; flex-wrap: wrap; gap: var(--column-gap); justify-content: flex-start; align-content: flex-start; contain: content; }
.x6o7n8i.x1unhpq9.x1hc1fzr > div > div > *, .x1xwk8fm.x193iq5w > div { width: calc((100% - (var(--column-gap) * (var(--column-count) - 1))) / var(--column-count)); max-height: var(--max-post-height); overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; }
@media (min-width: 1900px) { .x1qjc9v5.x71s49j.x1a2a7pz { width: 50%; max-width: none; margin: 0 25%; flex: 0 0 auto; } }
.x1daaz14.x1t2pt76, .xwib8y2.x1y1aw1k.xwya9rg, .xq1tmr.xvue9z > .x1yztbdb, footer { display: none; }
`;
}
// = ================= 控制面板創建 / CONTROL PANEL =================
function createControlPanel() {
if (!state.isActivePage) return;
if (state.panelCreated && resources.panel?.isConnected) return;
if (resources.panel && !resources.panel.isConnected) { resources.panel = null; state.panelCreated = false; }
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 createIconButton = (icon, key, action, title = '') => {
const btn = document.createElement('button');
Object.assign(btn.style, { padding: '8px 12px', border: 'none', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold', width: '40px', textAlign: 'center', fontSize: '16px', lineHeight: '1' });
btn.innerText = icon; btn.title = title;
const handler = () => {
state.buttons[key] = !state.buttons[key];
GM_setValue(`${key}Enabled`, state.buttons[key]);
updateButtonStyle(btn, state.buttons[key]);
action?.();
};
registerEventListener(btn, 'click', handler);
updateButtonStyle(btn, state.buttons[key]);
return btn;
};
const likeBtn = createIconButton('❤️', 'like', null, '自動點讚 / Auto-like');
const expandBtn = createIconButton('💬', 'otherExpand', () => { state.buttons.otherExpand ? startPeriodicCheck() : stopPeriodicCheck(); }, '自動展開留言 / Auto-expand comments');
const volumeBtn = createIconButton('🔊', 'volume', () => state.buttons.volume && processAllVideos(), '音量控制 / Volume control');
const volumeControlGroup = createControlGroup([createSmallButton('−', () => adjustVolume(-0.1)), createSmallButton('+', () => adjustVolume(0.1))]);
const columnBtn = createIconButton('🗂️', 'columns', () => { state.buttons.columns ? injectMultiColumnCSS() : removeMultiColumnCSS(); }, '多欄佈局 / Multi-column layout');
const columnControlGroup = createControlGroup([createSmallButton('−', () => adjustColumnCount(-1)), createSmallButton('+', () => adjustColumnCount(1))]);
const collapseBtn = createCollapseButton();
[likeBtn, expandBtn, volumeBtn, volumeControlGroup, columnBtn, columnControlGroup, collapseBtn].forEach(btn => panel.appendChild(btn));
document.body.appendChild(panel);
resources.panel = panel;
state.panelCreated = true;
if (state.panelCollapsed) togglePanelCollapse();
}
function setPanelVisibility(visible) {
if (!resources.panel?.isConnected) return;
resources.panel.style.display = visible ? 'flex' : 'none';
}
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', fontSize: '16px' });
btn.innerText = state.panelCollapsed ? '△' : '▽'; btn.title = '摺疊面板 / Toggle panel';
const handler = () => {
state.panelCollapsed = !state.panelCollapsed;
GM_setValue('panelCollapsed', state.panelCollapsed);
btn.innerText = state.panelCollapsed ? '△' : '▽';
togglePanelCollapse();
};
registerEventListener(btn, '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', lineHeight: '1' });
btn.innerText = text;
registerEventListener(btn, 'click', action);
return btn;
}
function updateButtonStyle(btn, isActive) {
Object.assign(btn.style, { backgroundColor: isActive ? '#1877f2' : '#e4e6eb', color: isActive ? 'white' : '#65676b' });
}
function togglePanelCollapse() {
const buttons = resources.panel?.querySelectorAll('button') || [];
buttons.forEach(btn => {
if (!['△', '▽', '+', '−'].includes(btn.innerText)) {
btn.style.display = state.panelCollapsed ? 'none' : 'block';
}
});
}
function registerEventListener(element, type, handler, options = undefined) {
element?.addEventListener?.(type, handler, options);
resources.eventHandlers.push({ element, type, handler, options });
}
// = ================= 功能邏輯 / FEATURE LOGIC =================
function applyColumnCount() { if (state.isActivePage && state.buttons.columns) injectMultiColumnCSS(); }
function adjustColumnCount(change) {
state.settings.columns = Math.max(1, state.settings.columns + change);
GM_setValue('COLUMN_COUNT', state.settings.columns);
applyColumnCount();
}
function adjustVolume(change) {
state.settings.volume = Math.min(1, Math.max(0, state.settings.volume + change));
GM_setValue('DEFAULT_VOLUME', state.settings.volume);
if (state.isActivePage && state.buttons.volume) processAllVideos();
}
function processAllVideos() {
resources.videoElements?.forEach(video => {
try { if (typeof video.volume === 'number') { video.volume = state.settings.volume; video.muted = false; } } catch {}
});
}
function startPeriodicCheck() {
if (!resources.intervals.periodicCheck && state.buttons.otherExpand) {
resources.intervals.periodicCheck = setInterval(() => { if (state.isActivePage) handleOtherButtons(); }, CONFIG.CHECK_INTERVAL);
}
}
function stopPeriodicCheck() {
if (resources.intervals.periodicCheck) { clearInterval(resources.intervals.periodicCheck); resources.intervals.periodicCheck = null; }
}
function getLikeButtonId(button) {
if (!button) return null;
if (button.matches(CONFIG.POST_LIKE_SELECTOR) || button.closest(CONFIG.POST_LIKE_SELECTOR)) {
const postStoreId = button.closest(CONFIG.POST_DATA_STORE_ID)?.dataset?.storeId;
if (postStoreId) return `post:${postStoreId}`;
}
return null;
}
function isLiked(button) {
if (!button) return false;
if (button.matches(CONFIG.COMMENT_LIKE_SELECTOR)) {
const span = button.querySelector(CONFIG.LIKE_SPAN_SELECTOR);
return span && !span.classList.contains(CONFIG.UNLIKED_COMMENT_CLASS);
}
const icon = button.querySelector(CONFIG.POST_LIKE_ICON_SELECTOR) || button.closest(CONFIG.POST_LIKE_SELECTOR)?.querySelector(CONFIG.POST_LIKE_ICON_SELECTOR);
if (icon) return icon.classList.contains(CONFIG.LIKED_POST_CLASS);
return button.getAttribute('aria-pressed') === 'true';
}
// = ================= 核心事件處理 (修正版) / CORE HANDLER =================
const throttledHandleMouseOver = throttle(handleMouseOver, CONFIG.THROTTLE_MS);
function handleMouseOver(event) {
if (!state.isActivePage) return;
const { clientX, clientY } = event;
const expandPx = CONFIG.TRIGGER_EXPAND_PX;
const target = event.target;
// = 1. 查看更多:快速路徑 + 範圍掃描 / See More: fast path + range scan
if (state.buttons.otherExpand && checkClickInterval()) {
const direct = target.closest?.(CONFIG.SEE_MORE_SELECTOR);
if (direct && direct.getAttribute('aria-expanded') !== 'true') {
safeClick(direct);
return;
}
for (const btn of document.querySelectorAll(CONFIG.SEE_MORE_SELECTOR)) {
if (btn.getAttribute('aria-expanded') === 'true') continue;
const rect = btn.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) continue;
if (isWithinExpandedRange(clientX, clientY, rect, expandPx)) {
safeClick(btn);
return;
}
}
}
// = 2. 自動點讚:快速路徑 + 範圍掃描 / Auto-like: fast path + range scan
if (state.buttons.like) {
const direct = target.closest?.(CONFIG.POST_LIKE_SELECTOR) || target.closest?.(CONFIG.COMMENT_LIKE_SELECTOR);
if (direct && !isLiked(direct) && isButtonVisible(direct)) {
const likeId = getLikeButtonId(direct);
if (!autoClickedLikeIds.has(likeId || '')) {
state.likeCoolingDown ? (state.pendingLikeTarget = { button: direct, id: likeId }) : executeLike(direct, likeId);
return;
}
}
for (const btn of document.querySelectorAll(`${CONFIG.POST_LIKE_SELECTOR}, ${CONFIG.COMMENT_LIKE_SELECTOR}`)) {
if (!isButtonVisible(btn) || isLiked(btn)) continue;
const rect = btn.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) continue;
if (isWithinExpandedRange(clientX, clientY, rect, expandPx)) {
const likeId = getLikeButtonId(btn);
if (!autoClickedLikeIds.has(likeId || '')) {
state.likeCoolingDown ? (state.pendingLikeTarget = { button: btn, id: likeId }) : executeLike(btn, likeId);
}
return;
}
}
}
}
function executeLike(button, likeId) {
if (!button?.isConnected) return;
safeClick(button);
recentlyClickedElements.add(button);
if (likeId) autoClickedLikeIds.add(likeId);
state.likeCoolingDown = true;
setTimeout(() => {
state.likeCoolingDown = false;
if (state.pendingLikeTarget?.button?.isConnected) {
const { button: pBtn, id: pId } = state.pendingLikeTarget;
if (!isLiked(pBtn) && !recentlyClickedElements.has(pBtn) && !autoClickedLikeIds.has(pId || '')) {
executeLike(pBtn, pId);
}
state.pendingLikeTarget = null;
}
}, CONFIG.LIKE_COOLDOWN);
}
function handleOtherButtons() {
if (!state.isActivePage || !state.buttons.otherExpand) return;
for (const btn of document.querySelectorAll(CONFIG.AUTO_EXPAND_SELECTOR)) {
if (checkClickInterval()) safeClick(btn);
}
}
function checkClickInterval() {
const now = Date.now();
if (now - state.lastClickTime > CONFIG.CLICK_INTERVAL) {
state.lastClickTime = now;
return true;
}
return false;
}
// = ================= 觀察器與導航 / OBSERVERS & NAV =================
const debouncedDomObserver = debounce(() => {
const now = Date.now();
if (now - state.lastObserverRun < CONFIG.OBSERVER_DEBOUNCE_MS) return;
state.lastObserverRun = now;
if (state.isActivePage) {
if (state.buttons.otherExpand) handleOtherButtons();
if (state.buttons.columns) applyColumnCount();
}
}, CONFIG.OBSERVER_DEBOUNCE_MS);
const debouncedVideoObserver = debounce(() => {
if (!state.isActivePage || !state.buttons.volume) return;
resources.videoElements = Array.from(document.querySelectorAll('video'));
processAllVideos();
}, CONFIG.OBSERVER_DEBOUNCE_MS);
function setupNavigationHandler() {
updatePageState();
const handleNavigation = debounce(() => {
const wasActive = state.isActivePage;
updatePageState();
if (state.isActivePage && !wasActive) {
createControlPanel(); setPanelVisibility(true);
if (state.buttons.otherExpand) startPeriodicCheck();
if (state.buttons.columns) injectMultiColumnCSS();
setupObservers();
} else if (!state.isActivePage && wasActive) {
stopPeriodicCheck(); removeMultiColumnCSS();
resources.observers.dom?.disconnect?.(); resources.observers.video?.disconnect?.();
resources.observers.dom = resources.observers.video = null;
setPanelVisibility(false);
} else if (state.isActivePage) {
createControlPanel(); setPanelVisibility(true);
if (state.buttons.columns) injectMultiColumnCSS();
}
}, CONFIG.DEBOUNCE_MS);
registerEventListener(window, 'popstate', handleNavigation);
registerEventListener(window, 'facebook:navigate', handleNavigation);
resources.intervals.navCheck = setInterval(() => { if (!document.hidden) handleNavigation(); }, CONFIG.NAVIGATION_CHECK_MS);
}
function updatePageState() { state.isActivePage = isAllowedPage(); }
function setupObservers() {
resources.observers.dom?.disconnect?.(); resources.observers.video?.disconnect?.();
resources.observers.dom = new MutationObserver(() => debouncedDomObserver());
resources.observers.dom.observe(document.body, { childList: true, subtree: true });
resources.observers.video = new MutationObserver(() => debouncedVideoObserver());
resources.observers.video.observe(document.body, { childList: true, subtree: true });
}
// = ================= 初始化 / INITIALIZATION =================
function init() {
setupNavigationHandler();
if (state.isActivePage) { createControlPanel(); setupObservers(); }
registerEventListener(document, 'mouseover', throttledHandleMouseOver);
registerEventListener(window, 'unload', cleanupResources);
registerEventListener(window, 'pagehide', cleanupResources);
}
if (document.readyState === 'complete') { init(); }
else { const loadHandler = () => { init(); window.removeEventListener('load', loadHandler); }; window.addEventListener('load', loadHandler); }
})();