Google Search 左側浮動篩選工具列

由於Google Search近期似乎又改動版面,原本使用的Google Search Sidebar UserScript已無法正常顯示,想說請AI幫我寫一個左側浮動篩選工具列比較快,如果您正好也有此需求,可以再把內容貼給AI,請他再依您的需求再改一改囉!

DEMO

file

程式碼
// ==UserScript==
// @name         Google Search Left Sidebar Filters (zh-TW) - v2.1
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  為 Google 搜尋結果頁面新增一個可浮動的左側篩選側邊欄。包含語言、地區(僅限台灣)、時間篩選功能。在瀏覽器寬度較小時自動隱藏。已排除圖片和購物頁面。所有文本均為台灣正體中文。此版本修正了篩選條件無法疊加的錯誤,確保多重篩選能同時作用。
// @author       Gemini
// @match        https://www.google.com/search?*
// @match        https://www.google.com.tw/search?*
// @grant        GM_addStyle
// ==/UserScript==

// 使用立即執行函數表達式 (IIFE) 包裹整個腳本,避免污染全域作用域
(function() {
    'use strict'; // 啟用嚴格模式,有助於撰寫更安全的 JavaScript 程式碼

    console.log('Google Search Left Sidebar Filters Userscript v2.1 啟動中...');

    // --- 核心邏輯:頁面類型檢查與早期退出 ---
    // 目的:根據使用者需求,在圖片 (udm=2) 和購物 (udm=28) 頁面不顯示側邊欄。
    const currentUrl = new URL(window.location.href); // 取得當前頁面的 URL 物件
    const udmParam = currentUrl.searchParams.get('udm'); // 從 URL 中獲取 'udm' 參數的值

    if (udmParam === '2' || udmParam === '28') {
        // 如果 udm 參數是 2 (圖片搜尋) 或 28 (購物搜尋),則打印訊息並終止腳本
        console.log(`當前頁面偵測到 udm=${udmParam} 參數。側邊欄將不會顯示。`);
        return; // 立即停止腳本的後續執行,不進行任何 DOM 操作或樣式注入
    }

    // --- 設定常量:側邊欄尺寸與響應式閾值 ---
    // 這些值是根據使用者先前的指示進行微調的。
    const SIDEBAR_WIDTH = '125px'; // 側邊欄的固定寬度。這是使用者指定的值。
    // MIN_BROWSER_WIDTH_FOR_SIDEBAR:當瀏覽器窗口寬度小於此值時,側邊欄將被隱藏。
    // 這是實現響應式設計的關鍵點,確保在小螢幕上內容不會被遮擋。
    const MIN_BROWSER_WIDTH_FOR_SIDEBAR = '1300px'; // 側邊欄顯示所需的最小瀏覽器寬度。這是使用者指定的值。

    // --- 樣式注入:使用 GM_addStyle 函數添加 CSS 規則 ---
    // GM_addStyle 是 Tampermonkey 提供的 API,用於將 CSS 樣式動態注入到頁面中。
    // 這樣做的好處是樣式可以被腳本控制,並且不會影響原始頁面的 CSS 文件。
    GM_addStyle(`
        /* #my-custom-sidebar:側邊欄容器的整體樣式 */
        #my-custom-sidebar {
            width: ${SIDEBAR_WIDTH}; /* 設定側邊欄的寬度,從上方常量獲取 */
            padding: 10px; /* 內部填充,使內容不緊貼邊緣 */
            box-sizing: border-box; /* 設置為 border-box 確保 padding 不會增加寬度 */
            border-right: 1px solid #dadce0; /* 右側邊框,視覺上區分側邊欄和主要內容 */
            background-color: #f8f9fa; /* 背景顏色,Google 介面常用的淺灰色 */
            position: fixed; /* 固定定位,使其在滾動時保持可見 */
            left: 0; /* 固定在頁面的最左邊 */
            top: 140px; /* 距離頁面頂部的距離。這是使用者確認的最終位置。 */
            /* 最大高度:計算為視口高度減去頂部距離和底部預留空間。
               這樣可以讓側邊欄在內容過多時顯示滾動條,而不是溢出。
               100vh 是視口高度,150px = top (140px) + 底部預留空間 (10px) */
            max-height: calc(100vh - 150px);
            overflow-y: auto; /* 垂直方向內容溢出時顯示滾動條 */
            overflow-x: hidden; /* 水平方向內容溢出時隱藏 */
            z-index: 9999; /* 確保側邊欄位於其他元素之上 */
            font-family: 'Google Sans', 'Roboto', Arial, sans-serif; /* 字體樣式,嘗試與 Google 官方字體保持一致 */
            box-shadow: 2px 0 5px rgba(0,0,0,0.1); /* 輕微的陰影,增加立體感 */
            display: none; /* 預設隱藏側邊欄,等待媒體查詢來顯示 */
        }

        /* 媒體查詢:控制側邊欄的響應式顯示/隱藏 */
        /* 目的:當瀏覽器窗口寬度足夠大時才顯示側邊欄。 */
        @media screen and (min-width: ${MIN_BROWSER_WIDTH_FOR_SIDEBAR}) {
            #my-custom-sidebar {
                display: block; /* 當螢幕寬度達到或超過設定閾值時,顯示側邊欄 */
            }
        }

        /* #my-custom-sidebar h3:側邊欄內每個篩選區塊標題(如「語言」、「國家/地區」)的樣式 */
        #my-custom-sidebar h3 {
            font-size: 15px; /* 標題字體大小 */
            color: #5f6368; /* 標題文字顏色 */
            /* margin-top: 調整區塊之間的距離。
               這是為了解決使用者「3 個區塊間增加少許距離」的需求。
               對非第一個 h3 應用此值,使其與前一個區塊拉開距離。 */
            margin-top: 20px;
            /* margin-bottom: 調整標題與下方選項的距離。
               這是為了解決使用者「區塊內標題跟選項減少距離」的需求。 */
            margin-bottom: 4px;
            padding-left: 0px; /* 確保左側沒有額外填充 */
            font-weight: normal; /* 標題字體不加粗,與 Google 介面風格一致 */
        }

        /* #my-custom-sidebar .sidebar-section:first-child h3:針對第一個篩選區塊標題的特殊樣式 */
        /* 目的:讓第一個區塊的標題(通常是「語言」)與側邊欄頂部的距離較小。
           這是為了解決使用者「藍框標示的空白」減少的需求,同時不影響後續區塊的間距。 */
        #my-custom-sidebar .sidebar-section:first-child h3 {
            margin-top: 5px; /* 第一個標題距離側邊欄頂部較小 */
        }

        /* #my-custom-sidebar a:側邊欄內每個篩選選項連結的樣式 */
        #my-custom-sidebar a {
            display: block; /* 讓連結佔據整行,便於點擊 */
            padding: 4px 0px; /* 上下填充,增加點擊區域和視覺空間 */
            color: #1a0dab; /* 連結的預設顏色 */
            text-decoration: none; /* 移除下劃線 */
            font-size: 13px; /* 字體大小 */
            border-radius: 4px; /* 輕微的圓角邊框,使選中效果更柔和 */
            white-space: nowrap; /* 防止文本換行 */
            overflow: hidden; /* 隱藏溢出內容 */
            text-overflow: ellipsis; /* 溢出內容顯示省略號 */
        }

        /* #my-custom-sidebar a:hover:連結滑鼠懸停時的樣式 */
        #my-custom-sidebar a:hover {
            background-color: #e8f0fe; /* 背景色變淺藍色 */
            color: #1a0dab; /* 文字顏色不變 */
        }

        /* #my-custom-sidebar a.selected:當前選中篩選連結的樣式 */
        #my-custom-sidebar a.selected {
            background-color: #e8f0fe; /* 選中時的背景色 */
            font-weight: bold; /* 字體加粗,明確標示選中狀態 */
            color: #1a0dab; /* 文字顏色不變 */
        }

        /* html, body:確保頁面整體沒有額外邊距,防止排版問題 */
        html, body {
            margin: 0 !important; /* 強制移除外部邊距 */
            padding: 0 !important; /* 強制移除內部填充 */
            min-width: 0 !important; /* 確保沒有最小寬度限制,避免水平滾動條 */
            overflow-x: hidden !important; /* 隱藏水平滾動條,避免因側邊欄導致頁面寬度溢出 */
        }
    `);
    console.log('自訂 CSS 樣式已注入 (v2.1)。');

    // --- 函數:創建並填充側邊欄內容 ---
    function createAndPopulateSidebar() {
        console.log('嘗試創建並填充側邊欄內容 (v2.1)。');

        // 檢查當前頁面是否為 Google 搜尋結果頁面所需的關鍵 DOM 元素是否存在
        // 如果不存在,則表示不在預期的頁面,直接返回
        if (!document.getElementById('search') && !document.getElementById('rcnt')) {
            console.log('未偵測到 Google 搜尋結果頁面關鍵元素或不在搜尋頁面。退出 createAndPopulateSidebar。');
            return;
        }

        // 獲取或創建側邊欄容器
        let sidebar = document.getElementById('my-custom-sidebar');
        if (!sidebar) {
            // 如果側邊欄不存在,則創建一個新的 div 元素並設置 ID
            sidebar = document.createElement('div');
            sidebar.id = 'my-custom-sidebar';
            // 將側邊欄添加到 <body> 的開頭,確保其在 DOM 中的位置。
            // prepend 比 append 更適合浮動元素,通常能減少與頁面原有佈局的衝突。
            document.body.prepend(sidebar);
            console.log('側邊欄容器已創建並插入到頁面。');
        } else {
            // 如果側邊欄已經存在,則清空其內容,以便重新生成(適用於 SPA 頁面導航)
            sidebar.innerHTML = ''; 
            console.log('側邊欄容器已存在,已清除內容準備重新填充。');
        }

        // 再次從 URL 中獲取當前搜尋關鍵字 'q'
        // 時間篩選選項只有在有搜尋關鍵字時才顯示
        const currentQuery = currentUrl.searchParams.get('q'); 

        // 輔助函數:向側邊欄添加一個篩選區塊 (如「語言」、「國家/地區」、「時間」)
        function addSectionToSidebar(title, filters) {
            const sectionDiv = document.createElement('div');
            sectionDiv.className = 'sidebar-section'; // 為每個篩選區塊添加一個類名,方便 CSS 定義

            const h3 = document.createElement('h3');
            h3.textContent = title; // 設置區塊標題
            sectionDiv.appendChild(h3); // 將標題添加到區塊 div 中

            // 遍歷每個篩選選項,創建對應的連結
            filters.forEach(filter => {
                const link = document.createElement('a');
                // 複製當前 URL,以便修改參數而不影響原始頁面狀態
                const url = new URL(window.location.href); 
                const params = new URLSearchParams(url.search); // 獲取 URL 的查詢參數

                // --- 核心修正:只針對當前篩選類型修改或刪除參數,保留其他篩選條件 ---
                if (filter.type === 'lang') {
                    if (filter.param) {
                        params.set('lr', filter.param); // 設定語言參數
                    } else { // 如果 param 為空,表示「不限語言」,則移除該參數
                        params.delete('lr');
                    }
                } else if (filter.type === 'country') {
                    if (filter.param) {
                        params.set('cr', filter.param); // 設定國家/地區參數
                    } else { // 如果 param 為空,表示「不限國家/地區」,則移除該參數
                        params.delete('cr');
                    }
                } else if (filter.type === 'time') {
                    if (filter.param) {
                        params.set('tbs', 'qdr:' + filter.param); // 設定時間參數 (格式為 qdr:參數值)
                        // 確保清除可能存在的其他時間相關參數,例如 as_qdr,以避免衝突
                        params.delete('as_qdr'); 
                    } else { // 如果 param 為空,表示「不限時間」,則移除該參數
                        params.delete('tbs');
                        params.delete('as_qdr'); // 同時移除 as_qdr
                    }
                }
                // --- 修正結束 ---

                url.search = params.toString(); // 將修改後的參數重新設置回 URL
                link.href = url.toString(); // 設置連結的 href 屬性
                link.textContent = filter.text; // 設置連結顯示的文本

                // 檢查當前篩選選項是否被選中,以便添加 'selected' 類名高亮顯示
                const currentLr = currentUrl.searchParams.get('lr');
                const currentCr = currentUrl.searchParams.get('cr');
                const currentTbs = currentUrl.searchParams.get('tbs');

                let isSelected = false;
                if (filter.type === 'lang') {
                    // 如果當前語言參數與篩選選項匹配,或當前沒有語言參數且篩選選項為「不限語言」
                    isSelected = (currentLr === filter.param) || (!currentLr && filter.param === '');
                } else if (filter.type === 'country') {
                    // 如果當前國家參數與篩選選項匹配,或當前沒有國家參數且篩選選項為「不限國家/地區」
                    isSelected = (currentCr === filter.param) || (!currentCr && filter.param === '');
                } else if (filter.type === 'time') {
                    // 如果當前時間參數與篩選選項匹配,或當前沒有時間參數且篩選選項為「不限時間」
                    isSelected = (currentTbs === ('qdr:' + filter.param)) || (!currentTbs && filter.param === '');
                }

                if (isSelected) {
                    link.classList.add('selected'); // 如果被選中,添加 'selected' 類名
                }

                sectionDiv.appendChild(link); // 將連結添加到區塊 div 中
            });

            sidebar.appendChild(sectionDiv); // 將完整的篩選區塊添加到側邊欄中
            console.log(`已添加篩選區塊: ${title}。篩選器數量: ${filters.length}`);
        }

        // --- 篩選選項的定義 ---
        // 1. 語言篩選
        addSectionToSidebar('語言', [
            { text: '不限語言', type: 'lang', param: '' }, // param 為空表示不設定語言參數
            { text: '所有中文網頁', type: 'lang', param: 'lang_zh-CN|lang_zh-TW' },
            { text: '繁體中文網頁', type: 'lang', param: 'lang_zh-TW' },
            { text: '英文網頁', type: 'lang', param: 'lang_en' }
        ]);

        // 2. 國家/地區篩選
        addSectionToSidebar('國家/地區', [
            { text: '不限國家/地區', type: 'country', param: '' }, // param 為空表示不設定國家參數
            { text: '台灣', type: 'country', param: 'countryTW' } // 台灣的國家代碼
        ]);

        // 3. 時間篩選 (只有有搜尋關鍵字時才顯示)
        // 目的:避免在沒有搜尋關鍵字時顯示時間篩選,因為那通常無意義。
        if (currentQuery) { // 檢查是否有搜尋關鍵字
            addSectionToSidebar('時間', [
                { text: '不限時間', type: 'time', param: '' }, // param 為空表示不設定時間參數
                { text: '過去 1 小時', type: 'time', param: 'h' },
                { text: '過去 24 小時', type: 'time', param: 'd' },
                { text: '過去 1 週', type: 'time', param: 'w' },
                { text: '過去 1 個月', type: 'time', param: 'm' },
                { text: '過去 1 年', type: 'time', param: 'y' }
            ]);
        } else {
             console.log('未偵測到搜尋關鍵字,跳過時間篩選區塊。');
        }

        console.log('側邊欄內容填充完成。');
    }

    // --- 腳本啟動與 DOM 載入時機控制 ---
    // 目的:確保在頁面完全載入或關鍵 DOM 元素可用後才執行側邊欄的創建,避免操作不存在的元素。
    // 使用 setTimeout 加上少量延遲,讓 Google 自己的頁面元素先渲染完成,減少衝突。
    if (document.readyState === 'loading') {
        // 如果 DOM 仍在載入中,則在 DOMContentLoaded 事件觸發後執行
        window.addEventListener('DOMContentLoaded', () => {
            setTimeout(createAndPopulateSidebar, 1500); // 延遲 1.5 秒執行
        });
    } else {
        // 如果 DOM 已經載入完成,則直接延遲執行
        setTimeout(createAndPopulateSidebar, 1500); // 延遲 1.5 秒執行
    }

    // --- MutationObserver:監聽 DOM 變化並重新創建側邊欄 ---
    // 目的:Google 搜尋頁面是單頁應用程式 (SPA)。
    // 當使用者在搜尋結果頁面內部導航 (例如點擊「新聞」、「影片」或使用上一頁/下一頁按鈕時),
    // 頁面內容可能會動態更新而不觸發完整頁面重新載入。
    // MutationObserver 監聽這些 DOM 變化,確保側邊欄在需要時重新生成和顯示。
    const observer = new MutationObserver((mutationsList, observer) => {
        let shouldRecreate = false; // 標誌變量,指示是否需要重新創建側邊欄
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                // 檢查是否有新的節點被添加到 DOM 中
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE && (node.id === 'search' || node.id === 'rcnt' || node.id === 'main' || node.classList.contains('GyAeaf') || node.id === 'hdtb')) {
                        // 如果新增的節點是 Google 搜尋頁面的主要內容區域,則標記為需要重新創建
                        shouldRecreate = true;
                        break;
                    }
                }
            } else if (mutation.type === 'attributes' && (mutation.target.id === 'search' || mutation.target.id === 'rcnt' || mutation.target.id === 'hdtb-msb' || mutation.target.classList.contains('GyAeaf') || mutation.target.id === 'hdtb')) {
                // 檢查主要內容區域的屬性是否有變化(例如,動態顯示/隱藏)
                shouldRecreate = true;
            }
            if (shouldRecreate) break; // 如果已確定需要重新創建,則跳出循環
        }

        // 如果側邊欄不存在,或者檢測到需要重新創建的 DOM 變化
        if (!document.getElementById('my-custom-sidebar') || shouldRecreate) {
            console.log('偵測到 DOM 變化或側邊欄丟失。正在重新創建側邊欄...');
            setTimeout(createAndPopulateSidebar, 200); // 延遲 200ms 後重新創建側邊欄
        }
    });

    // 配置並啟動 MutationObserver,監聽 <body> 元素及其子樹的變化
    observer.observe(document.body, { childList: true, subtree: true, attributes: true });
    console.log('MutationObserver 已啟動,用於監聽 DOM 變化。');

})();

如果您沒用過UserScript,可以先於瀏覽器安裝 Violentmonkey 或 Tampermonkey 延伸套件後,建立一個新的腳本,將程式碼貼入即可使用。

發佈留言