Facebook 自動展開與互動增強

混合觸發模式:滑鼠游標自動展開查看更多 + 點讚 + 影片音量調整 + 左下角控制面板

目前為 2025-05-14 提交的版本,檢視 最新版本

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