// ==UserScript==
// @name 全能搜索引擎切换助手
// @namespace http://tampermonkey.net/
// @version 1.8
// @description 在搜索结果页面添加智能搜索引擎切换功能,支持多种布局和自定义设置
// @match *://www.google.com/search*
// @match *://www.google.co*/search*
// @match *://www.google.com.*/search*
// @match *://google.*/search*
// @match https://www.google.com.hk/search*
// @match www.google.com.hk/search*
// @match *://www.bing.com/search*
// @match *://cn.bing.com/search*
// @match *://www.baidu.com/s*
// @match *://www.sogou.com/web*
// @match *://www.so.com/s*
// @match *://duckduckgo.com/*
// @match *://newtab/
// @match *://www.google.com/
// @match *://www.google.co*/
// @match *://www.google.com.*/
// @match about:blank
// @match chrome://newtab/
// @author Yuze
// @copyright 2025, Yuze (https://gf.qytechs.cn/users/Yuze Guitar)
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 记录调试信息
const DEBUG = true;
function log(...args) {
if (DEBUG) {
console.log('[搜索引擎切换助手]', ...args);
}
}
// 全局错误处理增强
let errorRecoveryAttempts = 0;
const MAX_RECOVERY_ATTEMPTS = 3;
const ERROR_RECOVERY_DELAY = 1000; // 毫秒
// 错误处理升级
function handleError(error, context) {
console.error(`[搜索引擎切换助手] 错误 (${context}):`, error);
// 记录更详细的错误信息
if (error && error.stack) {
console.error(`[搜索引擎切换助手] 错误堆栈:`, error.stack);
}
// 如果是特定类型的错误,尝试恢复
if (context === '创建搜索引擎切换器UI') {
// 尝试创建一个最小化版本的UI
try {
setTimeout(() => {
createMinimalUI();
}, 1000);
} catch (e) {
console.error('[搜索引擎切换助手] 无法创建备用UI:', e);
}
}
}
// 创建最小化版本的UI作为备选
function createMinimalUI() {
try {
const minimalUI = document.createElement('div');
minimalUI.id = 'search-engine-switcher-minimal';
minimalUI.style.position = 'fixed';
minimalUI.style.top = '10px';
minimalUI.style.left = '10px';
minimalUI.style.zIndex = '9999';
minimalUI.style.background = '#fff';
minimalUI.style.padding = '5px 10px';
minimalUI.style.borderRadius = '4px';
minimalUI.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
minimalUI.style.fontSize = '14px';
minimalUI.style.cursor = 'pointer';
minimalUI.textContent = '搜索引擎切换';
minimalUI.addEventListener('click', function() {
const { currentEngine, query } = getCurrentEngineAndQuery();
if (currentEngine && query) {
// 显示简单的引擎选择菜单
showSimpleEngineMenu(minimalUI, currentEngine, query);
}
});
document.body.appendChild(minimalUI);
} catch (error) {
console.error('[搜索引擎切换助手] 创建最小化UI失败:', error);
}
}
// 显示简单的引擎选择菜单
function showSimpleEngineMenu(anchor, currentEngine, query) {
try {
// 移除现有菜单
const existingMenu = document.getElementById('simple-engine-menu');
if (existingMenu) {
existingMenu.remove();
}
// 创建菜单
const menu = document.createElement('div');
menu.id = 'simple-engine-menu';
menu.style.position = 'absolute';
menu.style.top = '30px';
menu.style.left = '0';
menu.style.background = '#fff';
menu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
menu.style.borderRadius = '4px';
menu.style.padding = '5px 0';
menu.style.zIndex = '10000';
// 添加引擎选项
for (const engine of defaultEngines) {
if (engine.name === currentEngine.name) continue;
const item = document.createElement('div');
item.style.padding = '8px 15px';
item.style.cursor = 'pointer';
item.style.hoverBackground = '#f5f5f5';
item.textContent = engine.name;
item.addEventListener('click', function() {
const url = engine.url.replace('{query}', encodeURIComponent(query));
window.location.href = url;
});
item.addEventListener('mouseover', function() {
this.style.backgroundColor = '#f5f5f5';
});
item.addEventListener('mouseout', function() {
this.style.backgroundColor = '';
});
menu.appendChild(item);
}
// 添加到页面
anchor.appendChild(menu);
// 点击外部关闭菜单
document.addEventListener('click', function closeMenu(e) {
if (!menu.contains(e.target) && e.target !== anchor) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
});
} catch (error) {
console.error('[搜索引擎切换助手] 显示简单菜单失败:', error);
}
}
// 检测操作系统
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const modifierKey = isMac ? '⌘' : 'Alt+';
const modifierKeyCode = isMac ? 'metaKey' : 'altKey';
// 定义搜索引擎
const defaultEngines = [
{
name: 'Google',
icon: 'https://www.google.com/favicon.ico',
url: 'https://www.google.com/search?q={query}',
matchPattern: 'google\\.[^/]+/search',
queryParam: 'q',
shortcut: 'G'
},
{
name: 'Bing',
icon: 'https://www.bing.com/favicon.ico',
url: 'https://www.bing.com/search?q={query}',
matchPattern: 'bing\\.com/search',
queryParam: 'q',
shortcut: 'B'
},
{
name: '百度',
icon: 'https://www.baidu.com/favicon.ico',
url: 'https://www.baidu.com/s?wd={query}',
matchPattern: 'baidu\\.com/s',
queryParam: 'wd',
shortcut: 'D'
},
{
name: 'DuckDuckGo',
// 内置图标数据,确保在任何环境下都能显示
icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI0RFNTgzMyIgZD0iTTEyIDFDNS45MiAxIDEgNS45MiAxIDEyczQuOTIgMTEgMTEgMTEgMTEtNC45MiAxMS0xMVMxOC4wOCAxIDEyIDF6bTMuNTggMTQuMWMwIC4wOS0uMDUuMi0uMTMuMjdsLS45My43Ni0uMDIuMDJhLjM4LjM4IDAgMCAxLS4yOC4xMmgtNS4xM2MtLjIyIDAtLjQtLjE4LS40LS40di0uOTctLjAxYzAtLjIyLjE4LS40LjQtLjRoNS4xNWEuMzguMzggMCAwIDEgLjI2LjEybC4wMS4wMi45My43NmEuMzQuMzQgMCAwIDEgLjEzLjI3eiIvPjwvc3ZnPg==',
url: 'https://duckduckgo.com/?q={query}',
matchPattern: 'duckduckgo\\.com',
queryParam: 'q',
shortcut: 'K'
}
];
// 在defaultEngines数组后添加图标缓存系统
const iconCache = {
cache: {},
// 获取图标,优先从缓存获取
async getIcon(url) {
try {
// 如果是数据URL,直接返回
if (!url || url.startsWith('data:')) {
return url;
}
// 检查缓存
if (this.cache[url]) {
return this.cache[url];
}
// 尝试获取图标
try {
// 简单地返回原始URL,不进行转换
// 在实际应用中,这里可以添加图标转换为dataURL的逻辑
this.cache[url] = url;
return url;
} catch (error) {
console.warn(`无法获取图标: ${url}`, error);
return url;
}
} catch (e) {
console.warn('图标缓存系统错误', e);
return url;
}
},
// 从字符串生成哈希值的辅助函数
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
return Math.abs(hash).toString(16);
}
};
// 在defaultEngines数组后添加快捷搜索前缀配置
const quickSearchPrefixes = [
{
prefix: 'gh ',
name: 'GitHub',
url: 'https://github.com/search?q={query}&type=repositories',
icon: 'https://github.com/favicon.ico',
description: '在GitHub上搜索仓库'
},
{
prefix: 'so ',
name: 'Stack Overflow',
url: 'https://stackoverflow.com/search?q={query}',
icon: 'https://stackoverflow.com/favicon.ico',
description: '在Stack Overflow上搜索问题'
},
{
prefix: 'npm ',
name: 'NPM包',
url: 'https://www.npmjs.com/search?q={query}',
icon: 'https://static.npmjs.com/favicon.ico',
description: '搜索NPM包'
},
{
prefix: 'yt ',
name: 'YouTube',
url: 'https://youtube.com/results?search_query={query}',
icon: 'https://www.youtube.com/favicon.ico',
description: '在YouTube上搜索视频'
},
{
prefix: 'bili ',
name: '哔哩哔哩',
url: 'https://www.bilibili.com/search?keyword={query}',
icon: 'https://www.bilibili.com/favicon.ico',
description: '在B站搜索视频'
},
{
prefix: 'zh ',
name: '知乎',
url: 'https://www.zhihu.com/search?type=content&q={query}',
icon: 'https://static.zhihu.com/heifetz/favicon.ico',
description: '在知乎搜索'
}
];
// 添加CSS样式
function addStyles() {
try {
GM_addStyle(`
/* 主容器 - 增加拖拽支持 */
#search-engine-switcher {
position: fixed;
top: 120px;
left: 10px;
z-index: 9999;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
display: flex;
flex-direction: column;
gap: 12px;
transition: background 0.3s ease, box-shadow 0.3s ease;
min-width: 120px;
max-width: 180px;
cursor: move; /* 指示可拖动 */
touch-action: none; /* 触摸设备支持 */
user-select: none; /* 防止拖动时选中文本 */
}
/* 深色模式 */
@media (prefers-color-scheme: dark) {
#search-engine-switcher {
background: #333;
color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
#search-engine-switcher .engine-btn {
color: #eee;
}
#search-engine-switcher .engine-btn:hover {
background: #444;
}
#search-engine-switcher .collapse-btn {
color: #ccc;
}
#search-engine-switcher.collapsed {
background: rgba(51, 51, 51, 0.9);
}
}
/* 折叠状态 - 优化折叠后的外观 */
#search-engine-switcher.collapsed {
padding: 8px 10px;
min-width: unset;
max-width: unset;
width: auto;
}
#search-engine-switcher.collapsed .engine-container {
display: none;
}
#search-engine-switcher.collapsed .switcher-header {
margin: 0;
padding: 0;
}
#search-engine-switcher.collapsed .collapse-btn {
transform: rotate(180deg);
margin-left: 5px;
}
#search-engine-switcher.collapsed .switcher-title {
margin: 0;
font-size: 12px;
white-space: nowrap;
}
/* 头部样式 */
.switcher-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.switcher-title {
font-weight: bold;
font-size: 14px;
margin: 0;
user-select: none;
}
.settings-btn, .collapse-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
opacity: 0.7;
transition: opacity 0.2s;
}
.settings-btn:hover, .collapse-btn:hover {
opacity: 1;
}
.header-actions {
display: flex;
gap: 8px;
}
/* 搜索引擎按钮容器 */
.engine-container {
display: flex;
flex-direction: column;
gap: 8px;
}
/* 搜索引擎按钮 */
.engine-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
border: none;
background: none;
cursor: pointer;
color: #333;
font-size: 14px;
text-align: left;
transition: background 0.2s;
}
.engine-btn:hover {
background: #f0f0f0;
}
.engine-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
.shortcut {
margin-left: auto;
padding: 2px 4px;
border-radius: 3px;
background: #f0f0f0;
color: #666;
font-size: 10px;
}
@media (prefers-color-scheme: dark) {
.shortcut {
background: #444;
color: #ccc;
}
}
/* 设置面板样式 */
.settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.settings-container {
background: #fff;
border-radius: 8px;
padding: 20px;
width: 90%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.settings-container {
background: #333;
color: #fff;
}
}
.settings-header {
font-weight: bold;
font-size: 18px;
margin-bottom: 15px;
}
.settings-section {
margin-bottom: 20px;
}
.section-title {
font-weight: bold;
margin-bottom: 10px;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.settings-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.settings-button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.cancel-button {
background: #f0f0f0;
color: #333;
}
.save-button {
background: #4285f4;
color: white;
}
@media (prefers-color-scheme: dark) {
.cancel-button {
background: #555;
color: #eee;
}
}
/* Mac选项 */
.mac-options {
display: flex;
gap: 15px;
margin-top: 10px;
}
.mac-option {
display: flex;
align-items: center;
gap: 5px;
}
/* 拖动手柄样式 */
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-right: 5px;
opacity: 0.5;
cursor: move;
transition: opacity 0.2s;
}
.drag-handle:hover {
opacity: 0.8;
}
.drag-handle svg {
width: 14px;
height: 14px;
}
/* 快捷搜索提示样式 */
#quick-search-hint {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
border: 1px solid #e1e4e8;
color: #24292e;
}
@media (prefers-color-scheme: dark) {
#quick-search-hint {
background-color: #333;
color: #fff;
border-color: #444;
}
#quick-search-hint div[style*="color: #666"] {
color: #aaa !important;
}
}
`);
log('样式已添加');
} catch (error) {
handleError(error, '添加样式');
}
}
// 获取启用的搜索引擎
function getEnabledEngines() {
try {
const settings = loadSettings();
const enabledEngineNames = settings.enabledEngines || defaultEngines.map(e => e.name);
return defaultEngines.filter(engine =>
enabledEngineNames.includes(engine.name)
);
} catch (error) {
handleError(error, '获取启用的搜索引擎');
return defaultEngines;
}
}
// 加载设置
function loadSettings() {
try {
const defaultSettings = {
enabledEngines: defaultEngines.map(engine => engine.name),
position: { top: 120, left: 10 },
autoPosition: true,
collapsed: false,
keyboardShortcuts: true,
macModifierType: 'command',
quickSearchEnabled: true // 添加快捷搜索开关
};
// 尝试从GM_getValue加载
let savedSettings = null;
try {
savedSettings = GM_getValue('searchEngineSettings');
if (savedSettings) {
log('从GM_getValue加载设置成功');
}
} catch (e) {
log('从GM_getValue加载设置失败', e);
}
// 如果GM_getValue失败,尝试从localStorage加载
if (!savedSettings) {
try {
const localStorageSettings = localStorage.getItem('searchEngineSwitcherSettings');
if (localStorageSettings) {
savedSettings = localStorageSettings;
log('从localStorage加载设置成功');
}
} catch (e) {
log('从localStorage加载设置失败', e);
}
}
// 如果没有找到保存的设置,使用默认设置
if (!savedSettings) {
log('未找到保存的设置,使用默认设置');
return defaultSettings;
}
// 解析设置
try {
let parsedSettings;
if (typeof savedSettings === 'string') {
parsedSettings = JSON.parse(savedSettings);
} else {
parsedSettings = savedSettings;
}
// 确保position是有效的
if (!parsedSettings.position || typeof parsedSettings.position !== 'object') {
parsedSettings.position = defaultSettings.position;
}
// 合并默认设置和保存的设置
const mergedSettings = {
...defaultSettings,
...parsedSettings
};
log('成功加载设置:', mergedSettings);
return mergedSettings;
} catch (e) {
log('解析设置失败,使用默认设置', e);
return defaultSettings;
}
} catch (error) {
handleError(error, '加载设置');
return {
enabledEngines: defaultEngines.map(engine => engine.name),
position: { top: 120, left: 10 },
autoPosition: true,
collapsed: false,
keyboardShortcuts: true,
macModifierType: 'command',
quickSearchEnabled: true
};
}
}
// 保存设置
function saveSettings(settings) {
try {
// 确保position是有效的
if (!settings.position || typeof settings.position !== 'object') {
settings.position = { top: 120, left: 10 };
}
// 确保top和left是数字
settings.position.top = parseInt(settings.position.top) || 120;
settings.position.left = parseInt(settings.position.left) || 10;
// 记录保存的设置
log('保存设置:', settings);
// 尝试使用GM_setValue保存
let gmSaveSuccess = false;
try {
GM_setValue('searchEngineSettings', settings);
gmSaveSuccess = true;
log('使用GM_setValue保存设置成功');
} catch (e) {
log('使用GM_setValue保存设置失败', e);
}
// 同时保存到localStorage作为备份
try {
localStorage.setItem('searchEngineSwitcherSettings', JSON.stringify(settings));
log('使用localStorage备份设置成功');
} catch (e) {
log('使用localStorage备份设置失败', e);
}
return gmSaveSuccess;
} catch (error) {
handleError(error, '保存设置');
return false;
}
}
// 获取当前搜索引擎和查询词
function getCurrentEngineAndQuery() {
try {
const url = window.location.href;
let currentEngine = null;
let query = '';
// 遍历搜索引擎进行匹配
for (const engine of defaultEngines) {
// 创建正则表达式
const regexPattern = new RegExp(engine.matchPattern, 'i');
if (regexPattern.test(url)) {
log(`匹配到搜索引擎: ${engine.name}`);
currentEngine = engine;
// 提取查询词
const urlObj = new URL(url);
query = urlObj.searchParams.get(engine.queryParam) || '';
// 特殊处理DuckDuckGo
if (engine.name === 'DuckDuckGo' && !query) {
// 检查URL中是否包含q=参数(可能在hash中)
if (url.includes('q=')) {
const match = url.match(/[?&#]q=([^&#]*)/);
if (match && match[1]) {
query = decodeURIComponent(match[1]);
}
}
}
break;
}
}
if (currentEngine) {
log(`当前引擎: ${currentEngine.name}, 查询词: ${query}`);
} else {
log('未匹配到搜索引擎');
}
return { currentEngine, query };
} catch (error) {
handleError(error, '获取当前搜索引擎和查询词');
return { currentEngine: null, query: '' };
}
}
// 切换到指定搜索引擎
function switchToEngine(engine, query) {
try {
if (!query) {
log('没有查询词,取消切换');
return;
}
// 将查询参数编码并替换到URL中
const encodedQuery = encodeURIComponent(query);
const targetUrl = engine.url.replace('{query}', encodedQuery);
log(`切换到 ${engine.name}, URL: ${targetUrl}`);
// 导航到新的URL
window.location.href = targetUrl;
} catch (error) {
handleError(error, '切换搜索引擎');
}
}
// 自动定位函数 - 新增
function calculateOptimalPosition() {
try {
const { currentEngine } = getCurrentEngineAndQuery();
if (!currentEngine) return { top: 120, left: 10 };
let top = 120, left = 10;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 针对不同搜索引擎的特殊定位逻辑
if (/google/.test(currentEngine.name.toLowerCase())) {
// Google搜索页面,放在搜索框下方
const searchBox = document.querySelector('form[role="search"]');
if (searchBox) {
const rect = searchBox.getBoundingClientRect();
top = rect.bottom + 20;
}
} else if (/baidu/.test(currentEngine.name.toLowerCase())) {
// 百度搜索页面,放在右侧
left = windowWidth - 200;
top = 150;
} else if (/bing/.test(currentEngine.name.toLowerCase())) {
// Bing搜索页面,优化位置
const searchBox = document.querySelector('.b_searchbox');
if (searchBox) {
const rect = searchBox.getBoundingClientRect();
top = rect.bottom + 15;
}
} else if (/duckduckgo/.test(currentEngine.name.toLowerCase())) {
// DuckDuckGo搜索页面,放在左上角
top = 80;
}
// 确保没有超出窗口边界
top = Math.max(10, Math.min(windowHeight - 200, top));
left = Math.max(10, Math.min(windowWidth - 200, left));
return { top, left };
} catch (error) {
handleError(error, '计算最佳位置');
return { top: 120, left: 10 };
}
}
// 创建搜索引擎切换器UI
function createSwitcherUI() {
try {
// 检查当前页面是否为搜索结果页
const { currentEngine, query } = getCurrentEngineAndQuery();
if (!currentEngine || !query) {
log('未检测到搜索引擎或查询词,不创建UI');
return;
}
// 检查是否已存在搜索引擎切换器,如果存在则移除
let switcher = document.getElementById('search-engine-switcher');
if (switcher) {
log('搜索引擎切换器已存在,移除旧的');
switcher.remove();
}
// 获取设置
const settings = loadSettings();
// 获取位置信息
let position = settings.position;
if (settings.autoPosition) {
position = calculateOptimalPosition();
}
// 获取启用的搜索引擎
const enabledEngines = getEnabledEngines();
// 创建搜索引擎切换器容器
switcher = document.createElement('div');
switcher.id = 'search-engine-switcher';
if (settings.collapsed) {
switcher.classList.add('collapsed');
}
// 设置位置
switcher.style.top = `${position.top}px`;
switcher.style.left = `${position.left}px`;
// 创建标题栏
const header = document.createElement('div');
header.className = 'switcher-header';
// 添加拖动手柄
const dragHandle = document.createElement('div');
dragHandle.className = 'drag-handle';
dragHandle.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 9h14M5 15h14"></path></svg>';
dragHandle.title = '拖动';
header.appendChild(dragHandle);
// 创建标题
const title = document.createElement('div');
title.className = 'switcher-title';
title.textContent = '搜索引擎切换';
header.appendChild(title);
// 创建按钮容器
const headerActions = document.createElement('div');
headerActions.className = 'header-actions';
// 创建设置按钮
const settingsButton = document.createElement('button');
settingsButton.className = 'settings-btn';
settingsButton.title = '设置';
settingsButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>';
settingsButton.addEventListener('click', function(e) {
e.stopPropagation();
showSettingsPanel();
});
headerActions.appendChild(settingsButton);
// 创建折叠/展开按钮
const collapseButton = document.createElement('button');
collapseButton.className = 'collapse-btn';
collapseButton.title = settings.collapsed ? '展开' : '折叠';
collapseButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>';
collapseButton.addEventListener('click', function(e) {
e.stopPropagation();
// 切换折叠状态
switcher.classList.toggle('collapsed');
// 更新设置
const newSettings = {...settings, collapsed: switcher.classList.contains('collapsed')};
saveSettings(newSettings);
// 更新按钮提示
this.title = newSettings.collapsed ? '展开' : '折叠';
});
headerActions.appendChild(collapseButton);
header.appendChild(headerActions);
switcher.appendChild(header);
// 创建搜索引擎按钮容器
const engineContainer = document.createElement('div');
engineContainer.className = 'engine-container';
// 创建每个搜索引擎按钮
for (const engine of enabledEngines) {
// 如果是当前搜索引擎,则跳过
if (currentEngine && engine.name === currentEngine.name) {
continue;
}
// 创建按钮
const engineBtn = document.createElement('button');
engineBtn.className = 'engine-btn';
engineBtn.title = `在${engine.name}中搜索 "${query}"`;
engineBtn.addEventListener('click', function() {
switchToEngine(engine, query);
});
// 创建图标占位符
const iconPlaceholder = document.createElement('div');
iconPlaceholder.className = 'engine-icon-placeholder';
iconPlaceholder.style.width = '16px';
iconPlaceholder.style.height = '16px';
iconPlaceholder.style.display = 'flex';
iconPlaceholder.style.alignItems = 'center';
iconPlaceholder.style.justifyContent = 'center';
iconPlaceholder.style.backgroundColor = '#f0f0f0';
iconPlaceholder.style.borderRadius = '3px';
iconPlaceholder.style.fontSize = '10px';
iconPlaceholder.style.fontWeight = 'bold';
iconPlaceholder.textContent = engine.name.charAt(0);
engineBtn.appendChild(iconPlaceholder);
// 异步加载图标
loadEngineIcon(engineBtn, iconPlaceholder, engine);
// 添加搜索引擎名称
engineBtn.appendChild(document.createTextNode(engine.name));
// 如果启用快捷键,添加提示
const settings = loadSettings();
if (settings.keyboardShortcuts) {
const shortcutSpan = document.createElement('span');
shortcutSpan.className = 'shortcut';
shortcutSpan.textContent = `${modifierKey}${engine.shortcut}`;
engineBtn.appendChild(shortcutSpan);
}
engineContainer.appendChild(engineBtn);
}
// 如果没有其他搜索引擎按钮,添加一个提示
if (engineContainer.children.length === 0) {
const noEnginesMsg = document.createElement('div');
noEnginesMsg.textContent = '没有其他可用的搜索引擎';
noEnginesMsg.style.padding = '10px 0';
noEnginesMsg.style.color = '#888';
noEnginesMsg.style.fontSize = '12px';
noEnginesMsg.style.textAlign = 'center';
engineContainer.appendChild(noEnginesMsg);
}
switcher.appendChild(engineContainer);
// 添加到页面
document.body.appendChild(switcher);
// 设置拖拽功能
setupDraggable(switcher, dragHandle);
// 添加点击事件,防止冒泡
switcher.addEventListener('click', function(e) {
e.stopPropagation();
});
log('搜索引擎切换器UI已创建');
} catch (error) {
handleError(error, '创建搜索引擎切换器UI');
}
}
// 添加加载图标的辅助函数
async function loadEngineIcon(engineBtn, iconPlaceholder, engine) {
try {
// 获取图标URL (使用简单方式,不进行转换)
const iconUrl = engine.icon;
// 创建图标
const img = document.createElement('img');
img.src = iconUrl;
img.className = 'engine-icon';
img.alt = engine.name;
// 当图标加载完成时替换占位符
img.onload = function() {
if (iconPlaceholder.parentNode) {
iconPlaceholder.replaceWith(img);
}
};
img.onerror = function() {
// 图标加载失败时使用文本替代
const textIcon = document.createElement('div');
textIcon.className = 'engine-icon text-icon';
textIcon.textContent = engine.name.charAt(0);
if (iconPlaceholder.parentNode) {
iconPlaceholder.replaceWith(textIcon);
}
};
// 添加到DOM,但保持隐藏状态直到加载完成
if (iconPlaceholder.parentNode) {
iconPlaceholder.parentNode.appendChild(img);
img.style.display = 'none';
}
} catch (error) {
log(`加载图标出错: ${error.message}`);
// 出错时使用文本图标
try {
const textIcon = document.createElement('div');
textIcon.className = 'engine-icon text-icon';
textIcon.textContent = engine.name.charAt(0);
if (iconPlaceholder.parentNode) {
iconPlaceholder.replaceWith(textIcon);
}
} catch (e) {
// 忽略
}
}
}
// 设置拖拽功能 - 新增
function setupDraggable(element, handle) {
try {
if (!element || !handle) return;
let isDragging = false;
let offset = { x: 0, y: 0 };
let debounceTimer = null;
handle.addEventListener('mousedown', startDrag);
handle.addEventListener('touchstart', startDrag, { passive: false });
function startDrag(e) {
e.preventDefault();
e.stopPropagation();
// 获取初始位置
const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
const clientY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
const rect = element.getBoundingClientRect();
offset = {
x: clientX - rect.left,
y: clientY - rect.top
};
isDragging = true;
// 添加拖动和结束事件监听
document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchend', stopDrag);
// 添加正在拖动的样式
element.style.transition = 'none';
element.style.opacity = '0.8';
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
// 获取当前位置
const clientX = e.clientX || (e.touches && e.touches[0].clientX) || 0;
const clientY = e.clientY || (e.touches && e.touches[0].clientY) || 0;
// 计算新位置
let newLeft = clientX - offset.x;
let newTop = clientY - offset.y;
// 边界检查
const maxLeft = window.innerWidth - element.offsetWidth;
const maxTop = window.innerHeight - element.offsetHeight;
newLeft = Math.max(0, Math.min(maxLeft, newLeft));
newTop = Math.max(0, Math.min(maxTop, newTop));
// 应用新位置
element.style.left = `${newLeft}px`;
element.style.top = `${newTop}px`;
}
function stopDrag() {
if (!isDragging) return;
// 移除事件监听
document.removeEventListener('mousemove', drag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchend', stopDrag);
// 恢复样式
element.style.transition = '';
element.style.opacity = '';
// 获取最终位置并保存
const rect = element.getBoundingClientRect();
const finalPosition = {
top: rect.top,
left: rect.left
};
// 保存位置到设置
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const settings = loadSettings();
settings.position = finalPosition;
settings.autoPosition = false; // 用户手动拖动后禁用自动定位
saveSettings(settings);
log('拖动位置已保存', finalPosition);
}, 300);
isDragging = false;
}
log('拖拽功能已设置');
} catch (error) {
handleError(error, '设置拖拽功能');
}
}
// 显示设置面板
function showSettingsPanel() {
try {
log('显示设置面板');
// 获取当前设置
const settings = loadSettings();
// 创建设置覆盖层
const settingsOverlay = document.createElement('div');
settingsOverlay.className = 'settings-overlay';
// 创建设置容器
const settingsContainer = document.createElement('div');
settingsContainer.className = 'settings-container';
// 创建设置标题
const settingsHeader = document.createElement('div');
settingsHeader.className = 'settings-header';
settingsHeader.textContent = '搜索引擎切换工具设置';
settingsContainer.appendChild(settingsHeader);
// 创建搜索引擎选择部分
const enginesSection = document.createElement('div');
enginesSection.className = 'settings-section';
const enginesTitle = document.createElement('div');
enginesTitle.className = 'section-title';
enginesTitle.textContent = '选择要显示的搜索引擎:';
enginesSection.appendChild(enginesTitle);
const checkboxGroup = document.createElement('div');
checkboxGroup.className = 'checkbox-group';
// 添加每个搜索引擎的复选框
defaultEngines.forEach(engine => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.name = 'engine';
checkbox.value = engine.name;
checkbox.checked = settings.enabledEngines.includes(engine.name);
label.appendChild(checkbox);
label.appendChild(document.createTextNode(` ${engine.name}`));
checkboxGroup.appendChild(label);
});
enginesSection.appendChild(checkboxGroup);
settingsContainer.appendChild(enginesSection);
// 创建快捷键部分
const shortcutSection = document.createElement('div');
shortcutSection.className = 'settings-section';
const shortcutTitle = document.createElement('div');
shortcutTitle.className = 'section-title';
shortcutTitle.textContent = '快捷键设置:';
shortcutSection.appendChild(shortcutTitle);
const shortcutCheckbox = document.createElement('label');
const shortcutInput = document.createElement('input');
shortcutInput.type = 'checkbox';
shortcutInput.name = 'keyboardShortcuts';
shortcutInput.checked = settings.keyboardShortcuts;
shortcutCheckbox.appendChild(shortcutInput);
shortcutCheckbox.appendChild(document.createTextNode(' 启用键盘快捷键'));
shortcutSection.appendChild(shortcutCheckbox);
// Mac特定选项
if (isMac) {
const shortcutContent = document.createElement('div');
shortcutContent.style.marginTop = '10px';
shortcutContent.style.marginLeft = '20px';
shortcutContent.style.display = settings.keyboardShortcuts ? 'block' : 'none';
shortcutInput.addEventListener('change', function() {
shortcutContent.style.display = this.checked ? 'block' : 'none';
});
const shortcutDescription = document.createElement('div');
shortcutDescription.style.marginTop = '10px';
shortcutDescription.style.marginLeft = '20px';
shortcutDescription.style.display = settings.keyboardShortcuts ? 'block' : 'none';
// 根据平台和修饰键类型显示正确的快捷键描述
const shortcutModifier = isMac ?
(settings.macModifierType === 'command' ? '⌘ Command' : '⌥ Option') :
'Alt';
shortcutDescription.innerHTML = `按下 <b>${shortcutModifier}+搜索引擎首字母</b> 可快速切换搜索引擎<br><span style="color:#777;font-size:12px;">(例如: ${shortcutModifier}+B 切换到Bing)</span>`;
shortcutContent.appendChild(shortcutDescription);
const macOptions = document.createElement('div');
macOptions.className = 'mac-options';
// Command选项
const commandOption = document.createElement('label');
commandOption.className = 'mac-option';
const commandRadio = document.createElement('input');
commandRadio.type = 'radio';
commandRadio.name = 'macModifier';
commandRadio.value = 'command';
commandRadio.checked = settings.macModifierType === 'command';
commandOption.appendChild(commandRadio);
commandOption.appendChild(document.createTextNode(' ⌘ (Command)'));
// Option选项
const optionOption = document.createElement('label');
optionOption.className = 'mac-option';
const optionRadio = document.createElement('input');
optionRadio.type = 'radio';
optionRadio.name = 'macModifier';
optionRadio.value = 'option';
optionRadio.checked = settings.macModifierType === 'option';
optionOption.appendChild(optionRadio);
optionOption.appendChild(document.createTextNode(' ⌥ (Option)'));
macOptions.appendChild(commandOption);
macOptions.appendChild(optionOption);
shortcutContent.appendChild(macOptions);
shortcutSection.appendChild(shortcutContent);
} else {
const shortcutDescription = document.createElement('div');
shortcutDescription.style.marginTop = '10px';
shortcutDescription.style.marginLeft = '20px';
shortcutDescription.style.display = settings.keyboardShortcuts ? 'block' : 'none';
shortcutDescription.textContent = '按下 Alt + 快捷键字母 可快速切换搜索引擎';
shortcutInput.addEventListener('change', function() {
shortcutDescription.style.display = this.checked ? 'block' : 'none';
});
shortcutSection.appendChild(shortcutDescription);
}
settingsContainer.appendChild(shortcutSection);
// 创建位置设置部分
const positionSection = document.createElement('div');
positionSection.className = 'settings-section';
const positionTitle = document.createElement('div');
positionTitle.className = 'section-title';
positionTitle.textContent = '位置设置:';
positionSection.appendChild(positionTitle);
const positionCheckbox = document.createElement('label');
const positionInput = document.createElement('input');
positionInput.type = 'checkbox';
positionInput.name = 'autoPosition';
positionInput.checked = settings.autoPosition;
positionCheckbox.appendChild(positionInput);
positionCheckbox.appendChild(document.createTextNode(' 自动选择最佳位置'));
const positionDescription = document.createElement('div');
positionDescription.style.marginTop = '5px';
positionDescription.style.marginLeft = '20px';
positionDescription.style.fontSize = '12px';
positionDescription.style.color = '#666';
positionDescription.textContent = '禁用后可拖动切换器到任意位置';
positionSection.appendChild(positionCheckbox);
positionSection.appendChild(positionDescription);
settingsContainer.appendChild(positionSection);
// 创建折叠选项
const collapseSection = document.createElement('div');
collapseSection.className = 'settings-section';
const collapseCheckbox = document.createElement('label');
const collapseInput = document.createElement('input');
collapseInput.type = 'checkbox';
collapseInput.name = 'collapsed';
collapseInput.checked = settings.collapsed;
collapseCheckbox.appendChild(collapseInput);
collapseCheckbox.appendChild(document.createTextNode(' 默认折叠'));
collapseSection.appendChild(collapseCheckbox);
settingsContainer.appendChild(collapseSection);
// 添加快捷搜索设置部分
const quickSearchSection = document.createElement('div');
quickSearchSection.className = 'settings-section';
const quickSearchTitle = document.createElement('div');
quickSearchTitle.className = 'section-title';
quickSearchTitle.textContent = '快捷搜索跳转:';
quickSearchSection.appendChild(quickSearchTitle);
const quickSearchCheckbox = document.createElement('label');
const quickSearchInput = document.createElement('input');
quickSearchInput.type = 'checkbox';
quickSearchInput.name = 'quickSearchEnabled';
quickSearchInput.checked = settings.quickSearchEnabled !== false; // 默认启用
quickSearchCheckbox.appendChild(quickSearchInput);
quickSearchCheckbox.appendChild(document.createTextNode(' 启用关键词快捷搜索'));
quickSearchSection.appendChild(quickSearchCheckbox);
const quickSearchDescription = document.createElement('div');
quickSearchDescription.style.marginTop = '10px';
quickSearchDescription.style.marginLeft = '20px';
quickSearchDescription.style.fontSize = '12px';
quickSearchDescription.style.color = '#666';
quickSearchDescription.innerHTML = '例如: 输入 <b>gh react</b> 可快速跳转到GitHub搜索react<br>' +
'支持的前缀: gh (GitHub), yt (YouTube), bili (哔哩哔哩), zh (知乎), so (Stack Overflow), npm (NPM包)';
quickSearchSection.appendChild(quickSearchDescription);
settingsContainer.appendChild(quickSearchSection);
// 创建按钮
const actions = document.createElement('div');
actions.className = 'settings-actions';
const cancelButton = document.createElement('button');
cancelButton.className = 'settings-button cancel-button';
cancelButton.textContent = '取消';
cancelButton.addEventListener('click', function() {
settingsOverlay.remove();
});
const saveButton = document.createElement('button');
saveButton.className = 'settings-button save-button';
saveButton.textContent = '保存';
saveButton.addEventListener('click', function() {
// 收集设置
const newSettings = {
enabledEngines: Array.from(
settingsContainer.querySelectorAll('input[name="engine"]:checked')
).map(checkbox => checkbox.value),
keyboardShortcuts: settingsContainer.querySelector('input[name="keyboardShortcuts"]').checked,
collapsed: settingsContainer.querySelector('input[name="collapsed"]').checked,
autoPosition: settingsContainer.querySelector('input[name="autoPosition"]').checked,
quickSearchEnabled: settingsContainer.querySelector('input[name="quickSearchEnabled"]').checked
};
// Mac特定选项
if (isMac) {
const macModifier = settingsContainer.querySelector('input[name="macModifier"]:checked');
newSettings.macModifierType = macModifier ? macModifier.value : 'command';
}
// 保存设置
const success = saveSettings(newSettings);
if (success) {
settingsOverlay.remove();
// 刷新切换器界面
const switcher = document.getElementById('search-engine-switcher');
if (switcher) {
switcher.remove();
}
setTimeout(createSwitcherUI, 100);
}
});
actions.appendChild(cancelButton);
actions.appendChild(saveButton);
settingsContainer.appendChild(actions);
settingsOverlay.appendChild(settingsContainer);
document.body.appendChild(settingsOverlay);
// 点击外部区域关闭设置面板
settingsOverlay.addEventListener('click', function(e) {
if (e.target === settingsOverlay) {
settingsOverlay.remove();
}
});
log('设置面板已显示');
} catch (error) {
handleError(error, '显示设置面板');
}
}
// 设置键盘快捷键
function setupKeyboardShortcuts() {
try {
const settings = loadSettings();
if (!settings.keyboardShortcuts) {
log('快捷键功能已禁用');
return;
}
const { currentEngine, query } = getCurrentEngineAndQuery();
if (!currentEngine || !query) {
log('未检测到搜索引擎或查询词,不设置快捷键');
return;
}
// 根据平台显示正确的快捷键提示
const modifierKeyDisplay = isMac ?
(settings.macModifierType === 'command' ? '⌘+' : '⌥+') :
'Alt+';
log(`设置键盘快捷键: ${modifierKeyDisplay}字母`);
document.addEventListener('keydown', function(e) {
// 检查修饰键是否按下 - 根据用户设置和平台正确检测
let modifierPressed = false;
if (isMac) {
if (settings.macModifierType === 'command') {
modifierPressed = e.metaKey;
} else if (settings.macModifierType === 'option') {
modifierPressed = e.altKey;
} else {
modifierPressed = e.metaKey; // 默认使用Command键
}
} else {
modifierPressed = e.altKey; // 非Mac平台使用Alt键
}
// 如果没有按下修饰键,直接返回
if (!modifierPressed) return;
// 获取按下的键
const key = e.key.toUpperCase();
log(`检测到组合键: ${modifierKeyDisplay}${key}`);
// 跳过输入框中的按键
if (e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' ||
e.target.isContentEditable) {
return;
}
// 查找匹配的搜索引擎
const targetEngine = getEnabledEngines().find(engine =>
engine.shortcut && engine.shortcut.toUpperCase() === key &&
(!currentEngine || engine.name !== currentEngine.name)
);
if (targetEngine) {
// 阻止默认事件(如浏览器快捷键)
e.preventDefault();
e.stopPropagation();
// 切换到目标搜索引擎
switchToEngine(targetEngine, query);
return false;
}
}, true); // 使用捕获阶段确保优先处理
log('键盘快捷键设置完成');
} catch (error) {
handleError(error, '设置键盘快捷键');
}
}
// 设置DOM变化观察器
function setupMutationObserver() {
try {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// 检查是否需要重新创建UI
const switcherExists = document.querySelector('#search-engine-switcher');
if (!switcherExists) {
createSwitcherUI();
}
// 检查新添加的搜索框
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const inputs = node.querySelectorAll('input[type="text"], input[type="search"], input:not([type])');
if (inputs.length > 0) {
setupQuickSearch();
}
}
});
}
}
});
// 观察整个文档的变化
observer.observe(document.body, {
childList: true,
subtree: true
});
log('MutationObserver 已设置');
} catch (error) {
handleError(error, '设置 MutationObserver');
}
}
// 添加菜单项
function setupUserMenu() {
try {
GM_registerMenuCommand('搜索引擎切换工具设置', showSettingsPanel);
// 添加快捷搜索切换选项
const settings = loadSettings();
const menuText = settings.quickSearchEnabled ?
'✓ 快捷搜索跳转 (已启用)' :
'○ 快捷搜索跳转 (已禁用)';
GM_registerMenuCommand(menuText, function() {
const settings = loadSettings();
settings.quickSearchEnabled = !settings.quickSearchEnabled;
saveSettings(settings);
alert(`快捷搜索跳转已${settings.quickSearchEnabled ? '启用' : '禁用'}.\n刷新页面后生效.`);
location.reload();
});
log('用户菜单已设置');
} catch (error) {
handleError(error, '设置用户菜单');
}
}
// 添加URL监控功能实现快捷搜索
function setupURLMonitor() {
// 检查是否启用了快捷搜索
const settings = loadSettings();
if (!settings.quickSearchEnabled) {
log('快捷搜索跳转功能已禁用');
return;
}
log('启用快捷搜索跳转功能');
// 上次检查的URL
let lastCheckedURL = '';
// 检查当前URL是否符合快捷搜索模式
function checkCurrentURL() {
// 获取当前URL
const currentURL = window.location.href;
// 如果URL没有变化,跳过
if (currentURL === lastCheckedURL) {
return;
}
// 更新lastCheckedURL
lastCheckedURL = currentURL;
// 定义搜索引擎URL模式和查询参数
const searchEnginePatterns = [
{ pattern: /google\.[^/]+\/search/, paramName: 'q' },
{ pattern: /bing\.com\/search/, paramName: 'q' },
{ pattern: /baidu\.com\/s/, paramName: 'wd' },
{ pattern: /duckduckgo\.com/, paramName: 'q' },
{ pattern: /sogou\.com\/web/, paramName: 'query' },
{ pattern: /so\.com\/s/, paramName: 'q' }
];
// 检查是否匹配任何搜索引擎模式
for (const engine of searchEnginePatterns) {
if (engine.pattern.test(currentURL)) {
// 尝试从URL中提取查询参数
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get(engine.paramName);
// 检查查询是否以特定前缀开头
if (query) {
log('检测到搜索查询:', query);
// 检查前缀
for (const prefix of quickSearchPrefixes) {
if (query.startsWith(prefix.prefix)) {
// 提取搜索词
const searchTerm = query.substring(prefix.prefix.length).trim();
if (searchTerm.length > 0) {
log(`检测到快捷搜索: ${prefix.name}, 搜索词: ${searchTerm}`);
// 构建目标URL
const targetURL = prefix.url.replace('{query}', encodeURIComponent(searchTerm));
// 跳转到目标URL
log(`正在跳转到: ${targetURL}`);
window.location.href = targetURL;
return;
}
}
}
}
// 找到了搜索引擎但没有匹配前缀,不需要检查其他搜索引擎
break;
}
}
}
// 立即检查一次
checkCurrentURL();
// 设置定期检查
setInterval(checkCurrentURL, 200);
// 监听URL变化(支持单页应用)
let lastUrl = location.href;
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
checkCurrentURL();
}
});
observer.observe(document, {subtree: true, childList: true});
// 监听popstate事件(浏览器前进/后退)
window.addEventListener('popstate', checkCurrentURL);
log('URL监控已设置');
}
// 添加新标签页快捷搜索功能
function setupNewTabQuickSearch() {
// 检查是否是新标签页
if (!/newtab|about:blank|chrome:\/\/newtab/.test(window.location.href) &&
!(/google\.com\/?$/.test(window.location.href))) {
return;
}
log('检测到新标签页,设置快捷搜索');
// 创建搜索框
const searchContainer = document.createElement('div');
searchContainer.id = 'quick-search-container';
searchContainer.style.position = 'fixed';
searchContainer.style.top = '30%';
searchContainer.style.left = '50%';
searchContainer.style.transform = 'translate(-50%, -50%)';
searchContainer.style.zIndex = '9999';
searchContainer.style.width = '500px';
searchContainer.style.padding = '20px';
searchContainer.style.backgroundColor = '#fff';
searchContainer.style.borderRadius = '8px';
searchContainer.style.boxShadow = '0 4px 20px rgba(0,0,0,0.15)';
searchContainer.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
// 添加标题
const title = document.createElement('h2');
title.textContent = '快捷搜索';
title.style.margin = '0 0 15px 0';
title.style.color = '#333';
searchContainer.appendChild(title);
// 添加说明
const description = document.createElement('p');
description.textContent = '输入前缀+搜索词,按回车直接跳转到对应网站搜索结果';
description.style.margin = '0 0 15px 0';
description.style.color = '#666';
description.style.fontSize = '14px';
searchContainer.appendChild(description);
// 添加前缀列表
const prefixList = document.createElement('div');
prefixList.style.display = 'flex';
prefixList.style.gap = '10px';
prefixList.style.marginBottom = '15px';
for (const prefix of quickSearchPrefixes) {
const badge = document.createElement('div');
badge.style.padding = '3px 8px';
badge.style.backgroundColor = '#f0f0f0';
badge.style.borderRadius = '4px';
badge.style.fontSize = '13px';
badge.style.cursor = 'pointer';
badge.innerHTML = `<strong>${prefix.prefix}</strong> ${prefix.description}`;
badge.addEventListener('click', () => {
searchInput.value = prefix.prefix;
searchInput.focus();
});
prefixList.appendChild(badge);
}
searchContainer.appendChild(prefixList);
// 添加搜索框
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = '输入前缀+搜索词,如: gh cursor';
searchInput.style.width = '100%';
searchInput.style.padding = '10px 15px';
searchInput.style.fontSize = '16px';
searchInput.style.border = '1px solid #ddd';
searchInput.style.borderRadius = '4px';
searchInput.style.outline = 'none';
searchInput.style.boxSizing = 'border-box';
searchInput.addEventListener('focus', () => {
searchInput.style.borderColor = '#4285f4';
searchInput.style.boxShadow = '0 0 0 2px rgba(66, 133, 244, 0.2)';
});
searchInput.addEventListener('blur', () => {
searchInput.style.borderColor = '#ddd';
searchInput.style.boxShadow = 'none';
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const query = searchInput.value.trim();
// 检查是否匹配任何前缀
for (const prefixConfig of quickSearchPrefixes) {
if (query.startsWith(prefixConfig.prefix)) {
const searchTerm = query.substring(prefixConfig.prefix.length).trim();
if (searchTerm.length > 0) {
// 构建URL并跳转
const targetUrl = prefixConfig.url.replace('{query}', encodeURIComponent(searchTerm));
log(`新标签页快捷搜索跳转: ${targetUrl}`);
window.location.href = targetUrl;
return;
}
}
}
// 如果没有匹配任何前缀,使用默认搜索引擎
window.location.href = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
}
});
searchContainer.appendChild(searchInput);
// 添加到页面
document.body.appendChild(searchContainer);
// 自动聚焦搜索框
setTimeout(() => {
searchInput.focus();
}, 300);
}
// 添加 setupQuickSearch 函数的实现
function setupQuickSearch() {
try {
const settings = loadSettings();
if (!settings.quickSearchEnabled) {
log('快捷搜索功能已禁用');
return;
}
log('设置快捷搜索功能');
// 定义快捷搜索前缀和对应URL
const quickSearchPatterns = {
'gh ': 'https://github.com/search?q={query}',
'so ': 'https://stackoverflow.com/search?q={query}',
'yt ': 'https://www.youtube.com/results?search_query={query}',
'bili ': 'https://search.bilibili.com/all?keyword={query}',
'zh ': 'https://www.zhihu.com/search?type=content&q={query}',
'npm ': 'https://www.npmjs.com/search?q={query}'
};
// 查找页面上的搜索输入框
const searchInputs = document.querySelectorAll('input[type="text"], input[type="search"], input:not([type])');
searchInputs.forEach(input => {
// 避免重复添加事件监听
if (input.dataset.quickSearchEnabled) return;
input.dataset.quickSearchEnabled = 'true';
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
const value = this.value.trim();
// 检查是否匹配任何前缀
for (const [prefix, url] of Object.entries(quickSearchPatterns)) {
if (value.startsWith(prefix)) {
const query = value.substring(prefix.length).trim();
if (query) {
e.preventDefault();
e.stopPropagation();
window.location.href = url.replace('{query}', encodeURIComponent(query));
return;
}
}
}
}
}, true);
});
log('快捷搜索功能已设置');
} catch (error) {
handleError(error, '设置快捷搜索');
}
}
// 初始化
function initialize() {
try {
log('开始初始化搜索引擎切换助手');
// 添加全局错误处理
window.addEventListener('error', function(event) {
if (event.filename && event.filename.includes('search-engine-switcher')) {
console.error('[搜索引擎切换助手] 全局错误:', event.message);
return true; // 阻止默认处理
}
}, true);
// 添加unhandledrejection处理
window.addEventListener('unhandledrejection', function(event) {
handleError(event.reason || new Error('未处理的Promise拒绝'), 'Promise错误');
event.preventDefault();
});
addStyles();
setupUserMenu();
// 设置URL监控(新增)
setupURLMonitor();
// 根据页面类型选择功能
if (/newtab|about:blank|chrome:\/\/newtab/.test(window.location.href) ||
/google\.[^/]+\/?$/.test(window.location.href)) {
// 如果是新标签页,设置新标签页快捷搜索
setupNewTabQuickSearch();
} else {
// 如果是搜索结果页,创建搜索引擎切换器UI
setTimeout(() => {
try {
createSwitcherUI();
// 设置快捷搜索功能
setupQuickSearch();
// 然后设置键盘快捷键
setTimeout(() => {
try {
setupKeyboardShortcuts();
// 最后设置MutationObserver
setTimeout(() => {
try {
setupMutationObserver();
} catch (e) {
handleError(e, '设置MutationObserver');
}
}, 200);
} catch (e) {
handleError(e, '设置键盘快捷键');
}
}, 150);
} catch (e) {
handleError(e, '创建UI');
}
}, 100);
}
log('初始化完成');
} catch (error) {
handleError(error, '初始化');
}
}
// 立即执行初始化
initialize();
// 确保DOM加载后执行
if (document.readyState !== 'complete') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(createSwitcherUI, 300);
});
}
// 页面完全加载后再次执行
window.addEventListener('load', function() {
setTimeout(createSwitcherUI, 500);
});
})();