Facebook Worker's Wisdom DX

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)

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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); }
})();