Greasy Fork

来自缓存

Greasy Fork is available in English.

🔍 搜索快切

快速切换搜索引擎的工具栏,支持自定义搜索引擎

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==

// @name         🔍 搜索快切

// @namespace    Aiccest

// @version      1.0.0

// @description  快速切换搜索引擎的工具栏,支持自定义搜索引擎

// @author       Aiccest

// @match        *://*/*

// @grant        GM_addStyle

// @grant        GM_setValue

// @grant        GM_getValue

// @grant        GM_registerMenuCommand

// @noframes

// @license      MIT

// ==/UserScript==

(function() {

    'use strict';

    const CONFIG = { DELAY: 500, DEBOUNCE: 150 };

    function debounce(fn, wait) {

        let t;

        return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };

    }

    function escapeHTML(str) {

        return str.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' })[c] || c);

    }

    function generateId(name) {

        let h = 0;

        for (let i = 0; i < name.length; i++) h = ((h << 5) - h + name.charCodeAt(i)) | 0;

        return 'engine_' + Math.abs(h);

    }

    const SEARCH_RULES = {

        preset: [

            { domains: ['www.baidu.com', 'baidu.com', 'm.baidu.com'], pathTest: /^\/(s|s\/|wappass\/bdstatic\/|from|mobile\/)/, paramKeys: ['wd', 'word', 'q'], exclude: [/^\/tieba/, /^\/zhidao/, /^\/question/, /^\/passport/], isBaidu: true },

            { domains: ['www.google.*', '*.google.*'], pathTest: /^\/search\b/, paramKeys: ['q'] },

            { domains: ['m.sm.cn'], pathTest: /^\/s\b/, paramKeys: ['q'] },

            { domains: ['*.so.com'], pathTest: /^\/s\b/, paramKeys: ['q'] },

            { domains: ['sogou.com', 'm.sogou.com'], pathTest: /^\/(web|web\/searchList\.jsp)\b/, paramKeys: ['q', 'keyword'] },

            { domains: ['*.bing.com'], pathTest: /^\/search\b/, paramKeys: ['q'] },

            { domains: ['zhihu.com'], pathTest: /^\/search\b/, paramKeys: ['q'] },

            { domains: ['metaso.cn'], pathTest: /^\/$/, paramKeys: ['q'] }

        ],

        custom(e) {

            try {

                const u = new URL(e.link.replace('%s', ''));

                let k = '';

                new URLSearchParams(u.search).forEach((v, key) => { if (v === '') k = key; });

                return { domains: [u.hostname], pathTest: new RegExp(`^${u.pathname}`), paramKeys: [k] };

            } catch {

                return null;

            }

        }

    };

    function isSearchEnginePage() {

        const u = new URL(location.href), p = new URLSearchParams(u.search);

        const e = GM_getValue('universal_search_engines', []);

        return [...SEARCH_RULES.preset, ...e.map(SEARCH_RULES.custom).filter(Boolean)].some(r => {

            if (!r.domains.some(d => u.hostname.includes(d.replace('*.', '')))) return false;

            if (r.exclude?.some(ex => ex.test(u.pathname))) return false;

            if (r.pathTest && !r.pathTest.test(u.pathname)) return false;

            return r.paramKeys.some(k => p.has(k));

        });

    }

    class BaiduHandler {

        constructor(render) {

            this.lastQuery = null;

            this.render = render;

            this.init();

        }

        init() {

            ['pushState', 'replaceState'].forEach(m => {

                const o = history[m];

                history[m] = (...args) => { o.apply(history, args); this.handleChange('history'); };

            });

            window.addEventListener('popstate', () => this.handleChange('popstate'));

            this.observer = new MutationObserver(m => {

                if (m.some(r => Array.from(r.addedNodes).some(n => n.id === 'content_left' || n.classList?.contains('c-container') || n.querySelector?.('[data-click]')))) {

                    this.handleChange('dom');

                }

            });

            this.observer.observe(document.body, { childList: true, subtree: true });

        }

        handleChange(src) {

            debounce(() => {

                const q = this.getQuery();

                if (q && (q !== this.lastQuery || src === 'popstate' || src === 'dom')) {

                    this.lastQuery = q;

                    document.querySelector('#search-toolbox')?.remove();

                    this.render();

                }

            }, 300)();

        }

        getQuery() {

            const p = new URLSearchParams(location.search);

            let q = p.get('wd') || p.get('word') || p.get('q');

            if (!q) {

                const i = document.querySelector('input#kw, input[name="wd"], input[type="search"]');

                q = i?.value?.trim();

            }

            return q || document.title.replace(/(百度搜索|_百度搜索|-百度搜索).*$/, '').trim();

        }

        destroy() {

            this.observer?.disconnect();

        }

    }

    class SearchBox {

        constructor() {

            this.engines = GM_getValue('universal_search_engines') || [

                { name: 'Google', link: 'https://www.google.com/search?q=%s' },

                { name: '百度', link: 'https://www.baidu.com/s?wd=%s' },

                { name: '神马', link: 'https://m.sm.cn/s?q=%s' },

                { name: '360搜索', link: 'https://www.so.com/s?q=%s' },

                { name: '搜狗', link: 'https://m.sogou.com/web/searchList.jsp?keyword=%s' },

                { name: '必应', link: 'https://cn.bing.com/search?q=%s' },

                { name: '知乎', link: 'https://www.zhihu.com/search?q=%s' },

                { name: '秘塔AI', link: 'https://metaso.cn/?q=%s' }

            ].map(e => ({ ...e, id: generateId(e.name) }));

            if (!GM_getValue('universal_search_engines')) GM_setValue('universal_search_engines', this.engines);

            if (!isSearchEnginePage()) return;

            this.injectStyles();

            if (SEARCH_RULES.preset[0].domains.some(d => location.hostname.includes(d.replace('*.', '')))) {

                this.baiduHandler = new BaiduHandler(() => this.renderToolbox());

            }

            this.renderToolbox();

            this.bindEvents();

            this.bindResizeHandler();

            GM_registerMenuCommand('⚙️ 设置', () => this.showSettings());

        }

        injectStyles() {

            GM_addStyle(`

                #search-toolbox {

                    position: fixed; bottom: 0; left: 0; right: 0; background: rgba(255,255,255,0.98);

                    border-top: 1px solid #ddd; padding: 8px; display: flex; gap: 6px; z-index: 2147483647;

                    overflow-x: auto; scrollbar-width: none;

                }

                #search-toolbox::-webkit-scrollbar { display: none; }

                #search-toolbox.long-content { justify-content: flex-start; }

                #search-toolbox:not(.long-content) { justify-content: center; }

                .search-engine {

                    padding: 4px 10px; background: #007bff; color: #fff; border-radius: 4px; font-size: 12px;

                    white-space: nowrap; flex-shrink: 0; cursor: pointer;

                }

                #settings-btn { background: #6c757d; }

                @media (max-width: 480px) {

                    .search-engine { font-size: 11px; padding: 4px 8px; }

                }

            `);

        }

        renderToolbox() {

            let t = document.querySelector('#search-toolbox');

            if (!t) {

                t = document.createElement('div');

                t.id = 'search-toolbox';

                document.body.appendChild(t);

            }

            t.innerHTML = this.engines.map(e => `<div class="search-engine" data-id="${escapeHTML(e.id)}" data-link="${escapeHTML(e.link)}">${escapeHTML(e.name)}</div>`).join('') + '<div class="search-engine" id="settings-btn">⚙️</div>';

            this.updateToolbox();

        }

        bindEvents() {

            document.addEventListener('click', e => {

                const t = e.target;

                if (t.closest('#search-toolbox') && t.id === 'settings-btn') {

                    e.preventDefault();

                    const p = document.querySelector('#settings-panel-container');

                    if (p) {

                        p.remove();

                    } else {

                        this.showSettings();

                        setTimeout(() => {

                            document.addEventListener('click', function closePanel(ev) {

                                const path = ev.composedPath();

                                const isInPanel = path.some(el => el?.id === 'settings-panel-container' || el?.classList?.contains('settings-panel'));

                                const isSettingsBtn = ev.target.matches('#settings-btn');

                                if (!isInPanel && !isSettingsBtn) {

                                    document.querySelector('#settings-panel-container')?.remove();

                                    document.removeEventListener('click', closePanel);

                                }

                            }, { capture: true });

                        }, 50);

                    }

                } else if (t.classList.contains('search-engine') && t.id !== 'settings-btn') {

                    const q = this.getQuery();

                    if (q) window.open(t.dataset.link.replace('%s', encodeURIComponent(q)), '_blank');

                }

            });

        }

        getQuery() {

            if (this.baiduHandler) return this.baiduHandler.getQuery();

            const p = new URLSearchParams(location.search);

            for (const k of ['q', 'wd', 'query']) {

                const v = p.get(k);

                if (v?.trim()) return v.trim();

            }

            return document.querySelector('input[type="search"]')?.value?.trim() || '';

        }

        showSettings() {

            const p = document.createElement('div');

            p.id = 'settings-panel-container';

            document.body.appendChild(p);

            // 创建 Shadow DOM

            const shadow = p.attachShadow({ mode: 'open' });

            // 创建样式

            const style = document.createElement('style');

            style.textContent = `

                .settings-panel {

                    width: 60%; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white;

                    padding: 12px; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.15); z-index: 2147483647;

                    max-width: 600px; max-height: 80vh; overflow-y: auto; box-sizing: border-box;

                    font-family: Arial, sans-serif;

                }

                .settings-panel h3 {

                    margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid #eee; font-size: 12px;

                    display: flex; justify-content: space-between; align-items: center;

                }

                .engine-item {

                    margin-bottom: 12px; /* 保留条目间间距 */

                }

                .name-row {

                    display: flex; gap: 6px; align-items: center; margin-bottom: 0; /* 移除名称栏底部间距 */

                }

                .name-row input[type="text"] {

                    flex: 1; min-width: 60px; max-width: 120px; padding: 6px 8px; box-sizing: border-box;

                    border: 1px solid #ddd; border-radius: 4px;

                }

                .engine-actions {

                    display: flex; gap: 4px; margin-left: auto;

                }

                .url-input {

                    width: 100%; margin: 0; /* 移除URL输入框间距,使其紧贴名称栏 */

                    padding: 7px 10px; box-sizing: border-box;

                    border: 1px solid #ddd; border-radius: 4px;

                }

                .action-bar {

                    display: flex; gap: 6px; margin-top: 8px; padding-top: 12px; border-top: 1px solid #eee;

                }

                .action-btn {

                    flex: 1; padding: 5px; height: 24px; line-height: 14px; text-align: center; border-radius: 4px;

                    font-size: 16px; border: none; cursor: pointer;

                }

                .engine-actions button {

                    width: 18px; height: 24px; padding: 0; font-size: 14px; border-radius: 1px;

                    line-height: 22px; border: 1px solid #ddd; background: #f8f9fa;

                }

                .engine-actions button:hover { background: #e9ecef; }

                .engine-actions button[disabled] { opacity: 0.5; cursor: not-allowed; }

                #add-engine { background: #28a745; color: white; }

                #save-settings { background: #007bff; color: white; }

                #close-panel { background: #6c757d; color: white; }

                @media (prefers-color-scheme: dark) {

                    .settings-panel { background: #2d2d2d; color: #eee; }

                    .engine-actions button { background: #333; border-color: #555; }

                    .url-input { background: #333; color: #fff; border-color: #555; }

                    .name-row input[type="text"] { background: #333; color: #fff; border-color: #555; }

                    .action-bar { border-top: 1px solid #444; }

                    .settings-panel h3 { border-bottom: 1px solid #444; }

                }

            `;

            // 创建内容

            const content = document.createElement('div');

            content.className = 'settings-panel';

            content.innerHTML = `

                <h3>🔧 搜索引擎管理</h3>

                <div id="engine-list">

                    ${this.engines.map((e, i) => `

                        <div class="engine-item" data-id="${escapeHTML(e.id)}">

                            <div class="name-row">

                                <input type="text" value="${escapeHTML(e.name)}" required>

                                <div class="engine-actions">

                                    <button class="move-up" ${i === 0 ? 'disabled' : ''}>↑</button>

                                    <button class="move-down" ${i === this.engines.length - 1 ? 'disabled' : ''}>↓</button>

                                    <button class="delete">×</button>

                                </div>

                            </div>

                            <input class="url-input" type="url" value="${escapeHTML(e.link)}" required>

                        </div>

                    `).join('')}

                </div>

                <div class="action-bar">

                    <button class="action-btn" id="add-engine">添加</button>

                    <button class="action-btn" id="save-settings">保存</button>

                    <button class="action-btn" id="close-panel">关闭</button>

                </div>

            `;

            // 将样式和内容添加到 Shadow DOM

            shadow.appendChild(style);

            shadow.appendChild(content);

            // 绑定事件

            content.addEventListener('click', (e) => this.handleSettingsClick(e));

        }

        handleSettingsClick(e) {

            const t = e.target;

            const p = t.closest('.settings-panel');

            if (!p) return;

            if (t.id === 'add-engine') {

                const item = document.createElement('div');

                item.className = 'engine-item';

                item.dataset.id = generateId('新引擎');

                item.innerHTML = `

                    <div class="name-row">

                        <input type="text" placeholder="引擎名称" required>

                        <div class="engine-actions">

                            <button class="move-up">↑</button>

                            <button class="move-down">↓</button>

                            <button class="delete">×</button>

                        </div>

                    </div>

                    <input class="url-input" type="url" placeholder="https://www.google.com/search?q=%s" required>

                `;

                p.querySelector('#engine-list').appendChild(item);

                p.scrollTop = p.scrollHeight;

            } else if (t.id === 'save-settings') {

                const engines = [];

                let valid = true;

                p.querySelectorAll('.engine-item').forEach(item => {

                    const nameInput = item.querySelector('input[type="text"]');

                    const urlInput = item.querySelector('input[type="url"]');

                    const name = nameInput.value.trim();

                    const link = urlInput.value.trim();

                    if (!name) {

                        alert('引擎名称不能为空!');

                        nameInput.focus();

                        valid = false;

                        return;

                    }

                    if (!/%s/.test(link)) {

                        alert('URL必须包含%s占位符!');

                        urlInput.focus();

                        valid = false;

                        return;

                    }

                    try {

                        new URL(link.replace('%s', 'test'));

                    } catch {

                        alert('请输入有效的URL!');

                        urlInput.focus();

                        valid = false;

                        return;

                    }

                    engines.push({ id: item.dataset.id, name, link });

                });

                if (valid) {

                    this.engines = engines;

                    GM_setValue('universal_search_engines', engines);

                    document.querySelector('#settings-panel-container')?.remove();

                    document.querySelector('#search-toolbox')?.remove();

                    this.renderToolbox();

                }

            } else if (t.id === 'close-panel') {

                document.querySelector('#settings-panel-container')?.remove();

            } else if (t.classList.contains('move-up') || t.classList.contains('move-down')) {

                const item = t.closest('.engine-item');

                if (t.classList.contains('move-up')) {

                    item.previousElementSibling?.before(item);

                } else {

                    item.nextElementSibling?.after(item);

                }

                p.querySelectorAll('.engine-item').forEach((el, i) => {

                    el.querySelector('.move-up').disabled = i === 0;

                    el.querySelector('.move-down').disabled = i === p.querySelectorAll('.engine-item').length - 1;

                });

            } else if (t.classList.contains('delete')) {

                if (p.querySelectorAll('.engine-item').length <= 1) {

                    alert('至少保留一个搜索引擎!');

                    return;

                }

                t.closest('.engine-item').remove();

            }

        }

        updateToolbox() {

            const t = document.querySelector('#search-toolbox');

            if (!t) return;

            t.classList.remove('long-content');

            t.getBoundingClientRect();

            const sw = t.scrollWidth, cw = t.clientWidth;

            t.classList.toggle('long-content', sw > cw);

            if (sw > cw) t.scrollLeft = 0;

        }

        bindResizeHandler() {

            const h = debounce(() => {

                requestAnimationFrame(() => this.updateToolbox());

            }, CONFIG.DEBOUNCE);

            window.addEventListener('resize', h);

            window.addEventListener('orientationchange', h);

            requestAnimationFrame(() => this.updateToolbox());

        }

        destroy() {

            this.baiduHandler?.destroy();

            document.querySelectorAll('#search-toolbox, #settings-panel-container').forEach(e => e.remove());

        }

    }

    function init() {

        if (document.body && isSearchEnginePage()) new SearchBox();

        else setTimeout(init, 200);

    }

    init();

})();