// ==UserScript==
// @name 谷歌必应哔哩哔哩搜索引擎快速切换【自用】
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 在谷歌google、必应bing(包含cn版)、哔哩哔哩Bilibili搜索结果页添加可拖动、可锁定、可切换横向/竖向布局、可自定义按钮顺序的搜索工具栏。让AI写的自用脚本。
// @author Users & AI Assistant
// @match https://www.google.com/search*
// @match https://www.bing.com/search*
// @match https://cn.bing.com/search*
// @match https://search.bilibili.com/all*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-start
// @license MIT
// ==/UserScript==
/* jshint esversion: 8 */
(function() {
'use strict';
// =================================================================================
// --- 1. 全局配置 (Global Configuration) ---
// =================================================================================
// 在此数组中添加或修改搜索引擎
const engines = [
{ name: 'Google', url: 'https://www.google.com/search?q=' },
{ name: 'Bing', url: 'https://www.bing.com/search?q=' },
{ name: 'Bilibili', url: 'https://search.bilibili.com/all?keyword=' }
];
// 将引擎数组转换为Map,方便通过名称快速查找
const engineMap = new Map(engines.map(e => [e.name, e]));
// 用于在油猴脚本管理器中存储设置的键名
const LAYOUT_KEY = 'switcher_layout_v4'; // 存储布局模式 ('horizontal' / 'vertical')
const ORDER_KEY = 'switcher_engine_order_v4'; // 存储引擎按钮的顺序
const POSITIONING_KEY = 'switcher_positioning_v5';// 存储定位模式 ('fixed' / 'absolute')
// =================================================================================
// --- 2. 注册油猴菜单命令 (Register Tampermonkey Menu Commands) ---
// --- 此部分代码负责在油猴扩展的弹出菜单中创建设置选项 ---
// =================================================================================
(async () => {
// --- 菜单项1: 切换布局 ---
const currentLayout = await GM_getValue(LAYOUT_KEY, 'horizontal');
GM_registerMenuCommand(`[切换布局] 当前为: ${currentLayout === 'horizontal' ? '横向' : '竖向'}`, async () => {
await GM_setValue(LAYOUT_KEY, currentLayout === 'horizontal' ? 'vertical' : 'horizontal');
alert('布局模式已更改,请刷新页面以应用。');
});
// --- 菜单项2: 自定义引擎顺序 ---
GM_registerMenuCommand('[自定义] 引擎顺序', async () => {
const defaultOrder = engines.map(e => e.name).join(',');
const currentOrder = await GM_getValue(ORDER_KEY, defaultOrder);
const newOrderStr = prompt('请输入新的引擎顺序,用英文逗号 (,) 分隔。\n\n可用引擎: ' + defaultOrder, currentOrder);
if (newOrderStr === null) return; // 用户点击了取消
// 验证用户输入的合法性
const newOrderArray = newOrderStr.split(',').map(s => s.trim());
const newOrderSet = new Set(newOrderArray);
const defaultNameSet = new Set(engines.map(e => e.name));
if (newOrderSet.size !== defaultNameSet.size || ![...newOrderSet].every(name => defaultNameSet.has(name))) {
alert('输入错误!请确保所有引擎都已包含且名称正确。\n\n可用引擎: ' + defaultOrder);
return;
}
await GM_setValue(ORDER_KEY, newOrderStr);
alert('引擎顺序已更新,请刷新页面以应用。');
});
// --- 菜单项3: 切换定位模式 ---
const currentPositioning = await GM_getValue(POSITIONING_KEY, 'fixed');
GM_registerMenuCommand(`[切换定位] 当前为: ${currentPositioning === 'fixed' ? '固定屏幕' : '跟随页面'}`, async () => {
await GM_setValue(POSITIONING_KEY, currentPositioning === 'fixed' ? 'absolute' : 'fixed');
alert('定位模式已更改,请刷新页面以应用。');
});
})();
// =================================================================================
// --- 3. 动态样式生成 (Dynamic Style Generation) ---
// =================================================================================
/**
* 根据用户选择的布局和定位模式,生成对应的CSS样式字符串。
* @param {string} layout - 'horizontal' 或 'vertical'
* @param {string} positioning - 'fixed' 或 'absolute'
* @returns {string} CSS样式字符串
*/
function getStyles(layout, positioning) {
return `
#search-switcher-container {
position: ${positioning}; /* 'fixed': 固定在屏幕, 'absolute': 跟随页面滚动 */
width: auto;
background-color: rgba(245, 245, 247, 0.85);
backdrop-filter: blur(12px) saturate(1.2);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
flex-direction: ${layout === 'vertical' ? 'column' : 'row'}; /* 决定主容器是垂直还是水平 */
}
#search-switcher-header {
padding: 8px 6px;
cursor: move;
background-color: rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: ${layout === 'vertical' ? 'flex-start' : 'center'}; /* 竖向时内容居左, 横向时居中 */
user-select: none;
border-bottom: ${layout === 'vertical' ? '1px solid rgba(0, 0, 0, 0.1)' : 'none'};
border-right: ${layout === 'horizontal' ? '1px solid rgba(0, 0, 0, 0.1)' : 'none'};
border-radius: ${layout === 'vertical' ? '8px 8px 0 0' : '8px 0 0 8px'};
font-size: 14px;
color: #555;
}
#search-switcher-body {
padding: 8px;
display: flex;
gap: 6px;
align-items: center;
flex-direction: ${layout === 'vertical' ? 'column' : 'row'}; /* 按钮区域也同步方向 */
align-items: ${layout === 'vertical' ? 'stretch' : 'center'}; /* 竖向时按钮拉伸宽度 */
}
.search-switcher-btn {
padding: 5px 12px;
font-size: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 5px;
background-color: rgba(255, 255, 255, 0.7);
color: #333;
text-align: center;
white-space: nowrap;
transition: all 0.2s;
}
.search-switcher-btn:not(.search-switcher-btn-active) { cursor: pointer; }
.search-switcher-btn:not(.search-switcher-btn-active):hover { background-color: rgba(255, 255, 255, 1); border-color: rgba(0, 0, 0, 0.15); }
.search-switcher-btn-active { background-color: #e8f0fe; color: #5f6368; border-color: #d2e3fc; cursor: default; }
#lock-button {
cursor: pointer; font-size: 14px; border: none; background: none; padding: 0 4px;
line-height: 1; color: #555;
}
#lock-button:hover { color: #000; }
`;
}
/**
* 从当前页面的URL中提取搜索关键词。
* @returns {string} 搜索关键词,如果找不到则返回空字符串。
*/
function getQueryParam() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('q') || urlParams.get('keyword') || '';
}
// =================================================================================
// --- 4. UI创建与事件处理主函数 (Main Function for UI and Events) ---
// =================================================================================
async function createUI() {
const query = getQueryParam();
if (!query) return; // 如果不是搜索结果页,则不执行
// --- 步骤1: 读取所有用户配置 ---
const layout = await GM_getValue(LAYOUT_KEY, 'horizontal');
const positioning = await GM_getValue(POSITIONING_KEY, 'fixed');
const orderedEngineNames = (await GM_getValue(ORDER_KEY, engines.map(e => e.name).join(','))).split(',');
// --- 步骤2: 注入动态样式 ---
const styleSheet = document.createElement("style");
styleSheet.innerText = getStyles(layout, positioning);
document.head.appendChild(styleSheet);
// --- 步骤3: 创建DOM元素 (在内存中) ---
const container = document.createElement('div');
container.id = 'search-switcher-container';
const header = document.createElement('div'); // 拖动区域
header.id = 'search-switcher-header';
const body = document.createElement('div'); // 按钮容器
body.id = 'search-switcher-body';
const lockButton = document.createElement('button');
lockButton.id = 'lock-button';
// --- 步骤4: 根据配置创建引擎按钮 ---
const currentPageHostname = window.location.hostname;
orderedEngineNames.forEach(name => {
const engine = engineMap.get(name);
if (!engine) return; // 如果配置错误,安全跳过
const button = document.createElement('button');
button.className = 'search-switcher-btn';
button.textContent = engine.name;
// 检查当前按钮是否为当前网站,是则设为“活动”状态
let isActiveButton = false;
try {
const engineHostname = new URL(engine.url).hostname;
if (currentPageHostname === engineHostname || (engine.name === 'Bing' && currentPageHostname.endsWith('.bing.com'))) {
button.classList.add('search-switcher-btn-active');
isActiveButton = true;
}
} catch (e) {
console.warn('[Search Switcher] Error parsing engine URL:', engine.url, e);
}
// 非活动按钮才添加点击跳转事件
if (!isActiveButton) {
button.onclick = () => {
const currentQuery = getQueryParam(); // 在点击时重新获取关键词
window.location.href = engine.url + encodeURIComponent(currentQuery);
};
}
body.appendChild(button);
});
// --- 步骤5: 根据布局组装UI ---
if (layout === 'vertical') {
// 竖向时:锁按钮在头部,左侧对齐
header.appendChild(lockButton);
} else {
// 横向时:头部作为拖动柄,锁按钮在按钮行的最右侧
header.textContent = '⠿'; // Unicode拖动图标
body.appendChild(lockButton);
}
container.appendChild(header);
container.appendChild(body);
document.body.appendChild(container); // 最后将完整的UI添加到页面
// --- 步骤6: 位置恢复与事件绑定 ---
const posKey = `switcher_pos_v4_${currentPageHostname}`;
const lockKey = 'switcher_locked_v4';
let isLocked = await GM_getValue(lockKey, false);
let isDragging = false;
let offsetX, offsetY;
// 从存储中读取位置信息,并根据定位模式恢复
const savedPos = await GM_getValue(posKey, { top: '80px', left: 'auto', right: '20px' });
if (positioning === 'absolute') {
// 跟随页面模式:坐标需加上页面滚动距离
container.style.top = (parseInt(savedPos.top) || 0) + window.scrollY + 'px';
if (savedPos.left !== 'auto') {
container.style.left = (parseInt(savedPos.left) || 0) + window.scrollX + 'px';
container.style.right = 'auto';
} else {
container.style.right = savedPos.right;
container.style.left = 'auto';
}
} else {
// 固定屏幕模式:直接应用坐标
container.style.top = savedPos.top;
container.style.left = savedPos.left;
container.style.right = savedPos.right;
}
// 更新锁图标和拖动区域的鼠标样式
function updateLockState() {
lockButton.textContent = isLocked ? '🔒' : '🔓';
header.style.cursor = isLocked ? 'default' : 'move';
}
// 锁定按钮点击事件
lockButton.onclick = () => { isLocked = !isLocked; updateLockState(); GM_setValue(lockKey, isLocked); };
// 拖动开始事件
header.onmousedown = (e) => {
if (isLocked) return;
isDragging = true;
const rect = container.getBoundingClientRect(); // 获取元素相对于视窗的位置
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
container.style.right = 'auto'; // 拖动时统一使用left定位
e.preventDefault();
};
// 拖动过程事件
document.onmousemove = (e) => {
if (!isDragging || isLocked) return;
// 计算元素在视窗内的新位置
let newLeft = e.clientX - offsetX;
let newTop = e.clientY - offsetY;
// 边界检测,防止拖出屏幕
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
newTop = Math.max(0, Math.min(newTop, window.innerHeight - containerHeight));
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - containerWidth));
// 根据定位模式应用最终坐标
if (positioning === 'absolute') {
container.style.top = (newTop + window.scrollY) + 'px';
container.style.left = (newLeft + window.scrollX) + 'px';
} else {
container.style.top = newTop + 'px';
container.style.left = newLeft + 'px';
}
};
// 拖动结束事件
document.onmouseup = () => {
if (isDragging) {
isDragging = false;
// 核心:无论当前是什么模式,都保存相对于视窗的(fixed)坐标。
// 这样做可以确保在不同定位模式间切换时,位置保持一致。
const rect = container.getBoundingClientRect();
GM_setValue(posKey, { top: rect.top + 'px', left: rect.left + 'px', right: 'auto' });
}
};
// 初始化UI状态
updateLockState();
}
// =================================================================================
// --- 5. 脚本执行入口 (Script Execution Entry) ---
// --- 确保在DOM加载完成后再执行UI创建函数 ---
// =================================================================================
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', createUI);
} else {
createUI();
}
})();