Greasy Fork

Greasy Fork is available in English.

🌐 搜索中心增强

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

当前为 2025-05-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         🌐 搜索中心增强
// @name:en      🌐 Search Hub Enhancer
// @namespace    http://greasyfork.icu/zh-CN/users/1454800
// @version      1.0.5
// @description  快速切换搜索引擎的工具栏,可自定义引擎
// @description:en A toolbar for quick switching between search engines with custom engine support
// @author       Aiccest
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @noframes
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 配置
    const CONFIG = {
        STORAGE_KEY: 'search_hub_engines',
        DEBOUNCE_MS: 600,
        ANIMATION_MS: 300,
        TOOLBAR_POSITION: 'bottom-center',
    };

    // 语言包
    const i18n = {
        'zh-CN': {
            scriptName: '🌐 搜索中心增强',
            scriptDescription: '快速切换搜索引擎的工具栏,可自定义引擎',
            settingsTitle: '🌐 搜索引擎设置',
            addButton: '添加',
            saveButton: '保存',
            closeButton: '关闭',
            namePlaceholder: '名称',
            urlPlaceholder: '包含 %s 的URL',
            alertRequired: '名称和URL为必填项!',
            alertUrlFormat: 'URL必须包含%s占位符!',
            alertInvalidUrl: '无效的URL!',
            alertMinEngines: '至少需要一个搜索引擎!',
            alertNotSearchPage: '当前页面不是搜索页面,无法添加为搜索引擎!',
            alertNoEngineConfig: '无法检测当前页面的搜索引擎配置!',
            menuAddEngine: '🌐 添加当前页面为搜索引擎'
        },
        'en-US': {
            scriptName: '🌐 Search Hub Enhancer',
            scriptDescription: 'A toolbar for quickly switching search engines, with customizable engines',
            settingsTitle: '🌐 Search Engine Settings',
            addButton: 'Add',
            saveButton: 'Save',
            closeButton: 'Close',
            namePlaceholder: 'Name',
            urlPlaceholder: 'URL containing %s',
            alertRequired: 'Name and URL are required!',
            alertUrlFormat: 'URL must contain %s placeholder!',
            alertInvalidUrl: 'Invalid URL!',
            alertMinEngines: 'At least one search engine is required!',
            alertNotSearchPage: 'This page is not a search page and cannot be added as a search engine!',
            alertNoEngineConfig: 'Cannot detect the search engine configuration for this page!',
            menuAddEngine: '🌐 Add Current Page as Search Engine'
        }
    };

    // 获取系统语言
    const getLanguage = () => {
        const lang = navigator.language || navigator.userLanguage;
        return lang.startsWith('zh') ? 'zh-CN' : 'en-US';
    };

    const lang = getLanguage();

    // 全局 CSS
    const GLOBAL_CSS = `
        :host {
            --bg-color: rgba(255, 255, 255, 0.95);
            --text-color: #1f2937;
            --border-color: #e5e7eb;
            --hover-bg: #f9fafb;
            --panel-bg: white;
            --btn-bg: #f9fafb;
            --btn-active-bg: #e5e7eb;
            --btn-save-bg: #4f46e5;
            --btn-add-bg: #22c55e;
            --btn-close-bg: #6b7280;
        }

        @media (prefers-color-scheme: dark) {
            :host {
                --bg-color: rgba(31, 41, 55, 0.95);
                --text-color: #e5e7eb;
                --border-color: #4b5563;
                --hover-bg: #374151;
                --panel-bg: #1f2937;
                --btn-bg: #374151;
                --btn-active-bg: #4b5563;
            }
        }

        #search-hub-toolbar {
            position: fixed;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            background: var(--bg-color);
            backdrop-filter: blur(8px);
            border-radius: 12px 12px 0 0;
            padding: 8px;
            display: flex;
            gap: 8px;
            z-index: 2147483647;
            max-width: 90vw;
            overflow-x: auto;
            scrollbar-width: none;
            box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
            touch-action: pan-x;
            user-select: none;
            -webkit-user-select: none;
            pointer-events: auto;
        }

        #search-hub-toolbar::-webkit-scrollbar { display: none; }

        .engine-btn {
            padding: 6px 12px;
            background: var(--btn-bg);
            color: var(--text-color);
            border: 0.8px solid var(--border-color);
            border-radius: 8px;
            font-size: 14px;
            cursor: pointer;
            transition: background 0.2s ease;
            white-space: nowrap;
        }

        .engine-btn:hover { background: var(--hover-bg); }

        .settings-btn {
            background: var(--btn-bg);
            color: var(--text-color);
            border: 0.8px solid var(--border-color);
        }

        @media (max-width: 640px) {
            #search-hub-toolbar { max-width: 95vw; }
            .engine-btn { font-size: 12px; padding: 4px 8px; }
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
            to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
        }

        .settings-panel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: var(--panel-bg);
            border-radius: 12px;
            padding: 16px;
            box-shadow: 0 4px 16px rgba(0,0,0,0.2);
            z-index: 2147483647;
            max-width: 600px;
            max-height: 80vh;
            overflow-y: auto;
            font-family: system-ui, sans-serif;
            box-sizing: border-box;
            animation: fadeIn ${CONFIG.ANIMATION_MS}ms ease forwards;
            pointer-events: auto;
            color: var(--text-color);
        }

        h3 {
            font-size: 16px;
            margin: 0 0 12px;
            padding-bottom: 8px;
            border-bottom: 1px solid var(--border-color);
        }

        .engine-item {
            margin-bottom: 12px;
            border: 1px solid var(--border-color);
            border-radius: 6px;
            padding: 0;
        }

        .name-row {
            display: flex;
            gap: 8px;
            align-items: center;
            margin: 8px;
        }

        .name-row input {
            flex: 1;
            padding: 6px 8px;
            border: 1px solid var(--border-color);
            border-radius: 4px;
            font-size: 14px;
            box-sizing: border-box;
            background: var(--panel-bg);
            color: var(--text-color);
        }

        .url-input {
            width: calc(100% - 16px);
            margin: 0 8px 8px;
            padding: 6px 8px;
            border: 1px solid var(--border-color);
            border-radius: 4px;
            font-size: 14px;
            box-sizing: border-box;
            background: var(--panel-bg);
            color: var(--text-color);
        }

        .actions {
            display: flex;
            gap: 4px;
        }

        .action-btn {
            width: 24px;
            height: 24px;
            padding: 0;
            font-size: 14px;
            border: 1px solid var(--border-color);
            border-radius: 4px;
            background: var(--btn-bg);
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            box-sizing: border-box;
            color: var(--text-color);
        }

        .action-btn:hover {
            background: var(--btn-active-bg);
        }

        .action-btn:disabled {
            opacity: 0.5;
            Wcursor: not-allowed;
        }

        .panel-actions {
            display: flex;
            gap: 8px;
            margin-top: 12px;
            border-top: 1px solid var(--border-color);
            padding-top: 12px;
            justify-content: flex-end;
        }

        .panel-btn {
            padding: 8px 16px;
            font-size: 14px;
            border-radius: 6px;
            border: none;
            cursor: pointer;
            box-sizing: border-box;
            color: white;
        }

        .add-btn { background: var(--btn-add-bg); }
        .save-btn { background: var(--btn-save-bg); }
        .close-btn { background: var(--btn-close-bg); }

        @media (max-width: 640px) {
            .settings-panel { width: 90vw; }
            .name-row input { max-width: calc(100% - 94px); }
            .url-input { width: calc(100% - 16px); }
            .panel-btn { padding: 6px 12px; font-size: 12px; }
        }
    `;

    // 工具函数
    const debounce = (fn, ms) => {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => fn(...args), ms);
        };
    };

    const sanitize = str => str.replace(/[&<>"']/g, c => ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;'
    })[c]);

    const generateId = () => `se_${Math.random().toString(36).slice(2, 10)}`;

    // Baidu 特殊处理
    class BaiduHandler {
        static isBaidu() {
            return /baidu\.com$/.test(location.hostname);
        }

        static getQuery() {
            if (!this.isBaidu()) return null;
            const input = SearchDetector.getSearchInput();
            return input?.value?.trim() || new URLSearchParams(location.search).get('wd')?.trim() || '';
        }
    }

    // 搜索页面检测
    class SearchDetector {
        static cachedInput = null;

        static config = {
            domains: {
                'metaso.cn': { basePath: '/', queryParam: 'q', displayName: 'Metaso' },
                'www.baidu.com': { basePath: '/s', queryParam: 'wd', displayName: 'Baidu' },
                'www.yandex.com': { basePath: '/search', queryParam: 'text', displayName: 'Yandex' },
                'search.yahoo.com': { basePath: '/search', queryParam: 'p', displayName: 'Yahoo' },
                'www.startpage.com': { basePath: '/search', queryParam: 'q', displayName: 'Startpage' },
                'search.aol.com': { basePath: '/aol/search', queryParam: 'q', displayName: 'AOL' },
            },
            exclude: [
                { domain: /baidu\.com$/, paths: [/^\/(tieba|zhidao|question|passport)/] },
            ],
            commonQueryParams: ['q', 'wd', 'word', 'keyword', 'search', 'query', 'text', 'p'],
        };

        static getSearchInput() {
            if (!this.cachedInput) {
                this.cachedInput = document.querySelector(
                    'input[type="search"], input#kw, input[name="wd"], input[name="q"], input[name="search"], input[name="query"], input[name="text"], input[name="p"], input.search-input'
                );
            }
            return this.cachedInput;
        }

        static isSearchPage() {
            try {
                const url = new URL(location.href);
                const params = new URLSearchParams(url.search);

                for (const rule of this.config.exclude) {
                    if (rule.domain.test(url.hostname) && rule.paths.some(ex => ex.test(url.pathname))) {
                        return false;
                    }
                }

                const domainConfig = this.config.domains[url.hostname];
                if (domainConfig && domainConfig.basePath === url.pathname.split('?')[0]) {
                    return true;
                }

                const hasQueryParam = this.config.commonQueryParams.some(param => params.has(param) && params.get(param).trim());
                const hasSearchInput = !!this.getSearchInput()?.value?.trim();
                const hasSearchTitle = document.title.toLowerCase().includes('search') || document.title.includes('搜索');

                return hasQueryParam || hasSearchInput || hasSearchTitle;
            } catch (e) {
                console.error('SearchDetector.isSearchPage error:', e);
                return false;
            }
        }

        static getQuery() {
            try {
                const params = new URLSearchParams(location.search);
                for (const param of this.config.commonQueryParams) {
                    const value = params.get(param)?.trim();
                    if (value) return value;
                }

                const inputValue = this.getSearchInput()?.value?.trim();
                if (inputValue) return inputValue;

                return document.title.replace(/\s*[-_|](搜索|Search|Query|Results).*$/, '').trim();
            } catch (e) {
                console.error('SearchDetector.getQuery error:', e);
                return '';
            }
        }

        static custom(e) {
            try {
                const u = new URL(e.url.replace('%s', 'borrowers'));
                let k = '';
                new URLSearchParams(u.search).forEach((v, key) => { if (v === 'borrowers') k = key; });
                const pathTest = new RegExp(`^${u.pathname}(/.*)?$`);
                return { domains: [u.hostname], pathTest, paramKeys: [k || 'q'] };
            } catch {
                return null;
            }
        }

        static detectEngineConfig() {
            try {
                const url = new URL(location.href);
                const domainConfig = this.config.domains[url.hostname];
                if (domainConfig) {
                    const searchUrl = `${url.protocol}//${url.hostname}${domainConfig.basePath}${domainConfig.basePath === '/' ? '?' : domainConfig.basePath.includes('?') ? '&' : '?'}${domainConfig.queryParam}=%s`;
                    return { name: domainConfig.displayName, url: searchUrl };
                }

                let queryParam = null;
                let basePath = '/';
                let detectionSource = 'none';

                const forms = document.querySelectorAll('form[action]');
                const searchForm = Array.from(forms).find(form =>
                    form.querySelector('input[type="search"], input[name="q"], input[name="wd"], input[name="search"], input[name="query"], input[name="text"], input[name="p"], input.search-input')
                );

                if (searchForm) {
                    try {
                        const actionUrl = new URL(searchForm.action, url.origin);
                        basePath = actionUrl.pathname || '/';
                        const searchInput = searchForm.querySelector('input[type="search"], input[name="q"], input[name="wd"], input[name="search"], input[name="query"], input[name="text"], input[name="p"], input.search-input');
                        if (searchInput && searchInput.name) {
                            queryParam = searchInput.name;
                            detectionSource = 'form-input';
                        } else {
                            const actionParams = new URLSearchParams(actionUrl.search);
                            for (const param of this.config.commonQueryParams) {
                                if (actionParams.has(param)) {
                                    queryParam = param;
                                    detectionSource = 'form-action';
                                    break;
                                }
                            }
                        }
                    } catch (e) {
                        console.warn('Dynamic form detection failed:', e);
                    }
                }

                if (detectionSource === 'none') {
                    const params = new URLSearchParams(url.search);
                    for (const param of this.config.commonQueryParams) {
                        if (params.has(param) && params.get(param).trim()) {
                            queryParam = param;
                            break;
                        }
                    }

                    if (!queryParam) {
                        for (const [key, value] of params.entries()) {
                            if (value && value.trim().length > 0) {
                                queryParam = key;
                                break;
                            }
                        }
                    }

                    if (!queryParam && this.getSearchInput()?.value?.trim()) {
                        queryParam = 'q';
                    }

                    if (!queryParam) return null;

                    const pathSegments = url.pathname.split('/').filter(segment => segment);
                    const staticSegments = pathSegments.filter(segment =>
                        !/^[0-9]+$/.test(segment) &&
                        !/^ssid=/.test(segment) &&
                        !/^from=/.test(segment) &&
                        !/^[a-f0-9]{8}-/.test(segment) &&
                        segment.length < 20
                    );
                    basePath = staticSegments.length > 0 ? `/${staticSegments.join('/')}` : '/';
                    detectionSource = 'fallback';
                }

                if (!queryParam) return null;

                const hostnameParts = url.hostname.split('.');
                const commonSubdomains = ['www', 'm', 'mobile', 'search'];
                const tlds = ['com', 'cn', 'org', 'net', 'co', 'io', 'ai'];
                const significantParts = hostnameParts.filter(part =>
                    !commonSubdomains.includes(part) && !tlds.includes(part)
                );
                const engineName = significantParts.length > 0 ? significantParts[significantParts.length - 1] : hostnameParts[0];
                const displayName = engineName.charAt(0).toUpperCase() + engineName.slice(1);
                const baseUrl = `${url.protocol}//${url.hostname}${basePath}`;
                const searchUrl = `${baseUrl}${basePath === '/' ? '?' : basePath.includes('?') ? '&' : '?'}${queryParam}=%s`;

                return { name: displayName, url: searchUrl };
            } catch (e) {
                console.error('SearchDetector.detectEngineConfig error:', e);
                return null;
            }
        }
    }

    // 设置面板
    class SettingsPanel {
        constructor(searchHub) {
            this.searchHub = searchHub;
            this.panel = null;
        }

        render() {
            try {
                this.panel = document.createElement('div');
                this.panel.id = 'settings-panel-container';
                this.panel.setAttribute('translate', 'no');

                const shadow = this.panel.attachShadow({ mode: 'open' });
                const style = document.createElement('style');
                style.textContent = GLOBAL_CSS;

                const content = document.createElement('div');
                content.className = 'settings-panel';
                content.innerHTML = `
                    <h3>${i18n[lang].settingsTitle}</h3>
                    <div id="engine-list">
                        ${this.searchHub.engines.map((e, i) => `
                            <div class="engine-item" data-id="${sanitize(e.id)}">
                                <div class="name-row">
                                    <input type="text" value="${sanitize(e.name)}" placeholder="${i18n[lang].namePlaceholder}" required>
                                    <div class="actions">
                                        <button class="action-btn move-up" ${i === 0 ? 'disabled' : ''}>↑</button>
                                        <button class="action-btn move-down" ${i === this.searchHub.engines.length - 1 ? 'disabled' : ''}>↓</button>
                                        <button class="action-btn delete">×</button>
                                    </div>
                                </div>
                                <input class="url-input" type="url" value="${sanitize(e.url)}" placeholder="${i18n[lang].urlPlaceholder}" required>
                            </div>
                        `).join('')}
                    </div>
                    <div class="panel-actions">
                        <button class="panel-btn add-btn">${i18n[lang].addButton}</button>
                        <button class="panel-btn save-btn">${i18n[lang].saveButton}</button>
                        <button class="panel-btn close-btn">${i18n[lang].closeButton}</button>
                    </div>
                `;

                shadow.appendChild(style);
                shadow.appendChild(content);
                document.body.appendChild(this.panel);

                shadow.addEventListener('click', e => this.handleClick(e), { capture: true, passive: false });
            } catch (e) {
                console.error('SettingsPanel.render error:', e);
            }
        }

        handleClick(e) {
            e.stopPropagation();
            const target = e.target;
            const list = this.panel.shadowRoot.querySelector('#engine-list');

            if (target.classList.contains('add-btn')) {
                let name = lang === 'zh-CN' ? '新搜索引擎' : 'New Search Engine';
                let url = 'https://example.com/search?q=%s';

                if (SearchDetector.isSearchPage()) {
                    const engineConfig = SearchDetector.detectEngineConfig();
                    if (engineConfig) {
                        name = engineConfig.name;
                        url = engineConfig.url;
                    }
                }

                this.searchHub.addEngineItem(list, name, url);
                this.panel.scrollTop = this.panel.scrollHeight;
            } else if (target.classList.contains('save-btn')) {
                const engines = [];
                let valid = true;

                list.querySelectorAll('.engine-item').forEach(item => {
                    const name = item.querySelector('input[type="text"]')?.value.trim();
                    const url = item.querySelector('input[type="url"]')?.value.trim();

                    if (!name || !url) {
                        alert(i18n[lang].alertRequired);
                        valid = false;
                        return;
                    }

                    if (!/%s/.test(url)) {
                        alert(i18n[lang].alertUrlFormat);
                        valid = false;
                        return;
                    }

                    try {
                        new URL(url.replace('%s', 'test'));
                        engines.push({ id: item.dataset.id, name, url });
                    } catch {
                        alert(i18n[lang].alertInvalidUrl);
                        valid = false;
                    }
                });

                if (valid) {
                    this.searchHub.engines = engines;
                    GM_setValue(CONFIG.STORAGE_KEY, engines);
                    this.close();
                    this.searchHub.renderToolbar();
                }
            } else if (target.classList.contains('close-btn')) {
                this.close();
            } else if (target.classList.contains('move-up') || target.classList.contains('move-down')) {
                const item = target.closest('.engine-item');
                if (target.classList.contains('move-up')) {
                    item.previousElementSibling?.before(item);
                } else {
                    item.nextElementSibling?.after(item);
                }

                list.querySelectorAll('.engine-item').forEach((el, i) => {
                    el.querySelector('.move-up').disabled = i === 0;
                    el.querySelector('.move-down').disabled = i === list.querySelectorAll('.engine-item').length - 1;
                });
            } else if (target.classList.contains('delete')) {
                if (list.querySelectorAll('.engine-item').length <= 1) {
                    alert(i18n[lang].alertMinEngines);
                    return;
                }
                target.closest('.engine-item').remove();
            }
        }

        close() {
            if (this.panel) {
                this.panel.remove();
                console.log('Settings panel closed');
            }
            this.panel = null;
        }
    }

    // 主类
    class SearchHub {
        constructor() {
            this.engines = GM_getValue(CONFIG.STORAGE_KEY) || [
                { id: generateId(), name: 'Google', url: 'https://www.google.com/search?q=%s' },
                { id: generateId(), name: 'Bing', url: 'https://www.bing.com/search?q=%s' },
            ];

            if (!GM_getValue(CONFIG.STORAGE_KEY)) GM_setValue(CONFIG.STORAGE_KEY, this.engines);

            this.init();
        }

        init() {
            if (SearchDetector.isSearchPage()) {
                this.renderToolbar();
            }

            this.bindKeyboard();
            this.observePageChanges();

            GM_registerMenuCommand(i18n[lang].menuAddEngine, () => this.addCurrentPageAsEngine());
        }

        renderToolbar() {
            try {
                let toolbarContainer = document.querySelector('#search-hub-toolbar-container');
                if (toolbarContainer) {
                    toolbarContainer.remove();
                }
                if (!SearchDetector.isSearchPage()) return;

                toolbarContainer = document.createElement('div');
                toolbarContainer.id = 'search-hub-toolbar-container';
                toolbarContainer.setAttribute('translate', 'no');
                document.body.appendChild(toolbarContainer);

                const shadow = toolbarContainer.attachShadow({ mode: 'open' });
                const style = document.createElement('style');
                style.textContent = GLOBAL_CSS;

                const toolbar = document.createElement('div');
                toolbar.id = 'search-hub-toolbar';
                toolbar.innerHTML = `
                    ${this.engines.map(e => `
                        <button class="engine-btn" data-id="${sanitize(e.id)}" data-url="${sanitize(e.url)}">
                            ${sanitize(e.name)}
                        </button>
                    `).join('')}
                    <button class="engine-btn settings-btn">⚙️</button>
                `;

                toolbar.addEventListener('click', (e) => {
                    e.preventDefault();
                    const target = e.target;
                    if (target.classList.contains('settings-btn')) {
                        this.toggleSettings();
                    } else if (target.classList.contains('engine-btn')) {
                        const query = BaiduHandler.isBaidu() ? BaiduHandler.getQuery() : SearchDetector.getQuery();
                        if (query) window.open(target.dataset.url.replace('%s', encodeURIComponent(query)), '_blank');
                    }
                }, { capture: true, passive: false });

                shadow.appendChild(style);
                shadow.appendChild(toolbar);
            } catch (e) {
                console.error('SearchHub.renderToolbar error:', e);
            }
        }

        bindKeyboard() {
            this.keyboardHandler = (e) => {
                if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 's') {
                    e.preventDefault();
                    this.toggleSettings();
                    console.log('Keyboard shortcut triggered: Ctrl+Shift+S');
                }
            };
            document.addEventListener('keydown', this.keyboardHandler);
        }

        observePageChanges() {
            const updateToolbar = debounce(() => {
                const isSearchPage = SearchDetector.isSearchPage();
                const toolbarExists = !!document.querySelector('#search-hub-toolbar-container');

                if (isSearchPage && !toolbarExists) {
                    this.renderToolbar();
                } else if (!isSearchPage && toolbarExists) {
                    document.querySelector('#search-hub-toolbar-container')?.remove();
                }
            }, CONFIG.DEBOUNCE_MS);

            const observerTarget = document.querySelector('form, header, main') || document.body;
            const observer = new MutationObserver(mutations => {
                if (mutations.some(m => m.addedNodes.length || m.removedNodes.length)) {
                    updateToolbar();
                }
            });

            observer.observe(observerTarget, { childList: true, subtree: true });

            window.addEventListener('popstate', () => setTimeout(updateToolbar, 100));

            ['pushState', 'replaceState'].forEach(method => {
                const original = history[method];
                history[method] = (...args) => {
                    original.apply(history, args);
                    setTimeout(updateToolbar, 100);
                };
            });
        }

        toggleSettings() {
            const existingPanel = document.querySelector('#settings-panel-container');
            if (existingPanel) {
                this.closeSettings();
            } else {
                const settingsPanel = new SettingsPanel(this);
                settingsPanel.render();
                console.log('Settings panel opened');
            }
        }

        closeSettings() {
            const panel = document.querySelector('#settings-panel-container');
            if (panel) {
                panel.remove();
                console.log('Settings panel container removed');
            }
        }

        addEngineItem(list, name = lang === 'zh-CN' ? '新搜索引擎' : 'New Search Engine', url = 'https://example.com/search?q=%s', id = generateId()) {
            const item = document.createElement('div');
            item.className = 'engine-item';
            item.dataset.id = id;
            item.innerHTML = `
                <div class="name-row">
                    <input type="text" value="${sanitize(name)}" placeholder="${i18n[lang].namePlaceholder}" required>
                    <div class="actions">
                        <button class="action-btn move-up">↑</button>
                        <button class="action-btn move-down">↓</button>
                        <button class="action-btn delete">×</button>
                    </div>
                </div>
                <input class="url-input" type="url" value="${sanitize(url)}" placeholder="${i18n[lang].urlPlaceholder}" required>
            `;
            list.appendChild(item);
            return item;
        }

        addCurrentPageAsEngine() {
            if (!SearchDetector.isSearchPage()) {
                alert(i18n[lang].alertNotSearchPage);
                return;
            }

            const engineConfig = SearchDetector.detectEngineConfig();
            if (!engineConfig) {
                alert(i18n[lang].alertNoEngineConfig);
                return;
            }

            this.toggleSettings();
            const panel = document.querySelector('#settings-panel-container');
            if (panel) {
                const list = panel.shadowRoot.querySelector('#engine-list');
                this.addEngineItem(list, engineConfig.name, engineConfig.url);
                panel.scrollTop = panel.scrollHeight;
            }
        }

        destroy() {
            document.querySelector('#search-hub-toolbar-container')?.remove();
            this.closeSettings();
            if (this.keyboardHandler) {
                document.removeEventListener('keydown', this.keyboardHandler);
            }
        }
    }

    // 初始化
    function init() {
        try {
            new SearchHub();
        } catch (e) {
            console.error('SearchHub initialization error:', e);
        }
    }

    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }
})();