// ==UserScript==
// @name 快速搜索选中内容
// @name:en Quick Search Selected Text (URL Jump+Page Dark Detect)
// @namespace http://tampermonkey.net/
// @version 1.7
// @description 选中网页文本或双击Alt/Option键弹出搜索框,输入内容按回车默认Google搜索,输入网址按回车直接跳转。根据页面背景适配深/浅色模式。
// @description:en Double-press Alt/Option key for search popup. Enter searches Google by default, or navigates if input is a URL. Adapts to page background color.
// @author Ven (powered by Gemini)
// @match *://*/*
// @grant GM_addStyle
// @grant GM_openInTab
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 配置项 ---
const TRIGGER_KEY = 'Alt';
const DOUBLE_PRESS_INTERVAL = 300;
const DEFAULT_SEARCH_ENGINE_NAME = 'Google';
const DARK_MODE_LUMINANCE_THRESHOLD = 0.35;
const SEARCH_ENGINES = [
{ name: '百度', url: 'https://www.baidu.com/s?wd=' },
{ name: 'Google', url: 'https://www.google.com/search?q=' },
{ name: '淘宝', url: 'https://s.taobao.com/search?q=' },
{ name: '京东', url: 'https://search.jd.com/Search?keyword={query}&enc=utf-8&wq={query}' }
];
// --- 状态变量 ---
let lastKeyPressTime = 0;
let popupVisible = false;
let popupElement = null;
// --- 样式 (与 v1.6 相同) ---
GM_addStyle(`
/* --- 通用样式 --- */
#quick-search-popup-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.85);
z-index: 99999;
border-radius: 12px;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
min-width: 380px;
max-width: 650px;
opacity: 0;
transition: opacity 0.2s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
visibility: hidden;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
background-color: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px) saturate(150%);
-webkit-backdrop-filter: blur(12px) saturate(150%);
background-image: linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), rgba(245, 245, 255, 0.65));
border: 1px solid rgba(200, 200, 200, 0.5);
box-shadow:
0 0 10px 3px rgba(100, 150, 255, 0.15),
0 0 25px 8px rgba(200, 150, 255, 0.1),
0 4px 15px rgba(0, 0, 0, 0.1);
}
#quick-search-popup-container.visible {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
visibility: visible;
}
#quick-search-popup-container h3 {
margin: 0 0 15px 0;
padding-bottom: 10px;
font-size: 18px;
font-weight: 500;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
text-align: center;
color: #222;
cursor: default;
transition: color 0.3s ease, border-color 0.3s ease;
}
#quick-search-input {
width: 100%;
padding: 10px 12px;
border: 1px solid rgba(204, 204, 204, 0.8);
border-radius: 6px;
margin-bottom: 15px;
font-size: 15px;
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.8);
color: #333;
user-select: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}
#quick-search-input::placeholder {
color: #999;
transition: color 0.3s ease;
}
#quick-search-input:focus {
outline: none;
border-color: rgba(0, 123, 255, 0.8);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);
}
#quick-search-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.quick-search-button {
padding: 8px 15px;
border: none;
background-color: #007bff;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
text-align: center;
transition: background-color 0.2s ease, transform 0.1s ease;
flex-grow: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.quick-search-button:hover {
background-color: #0056b3;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.quick-search-button:active {
transform: translateY(0px);
background-color: #004085;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* --- 深色模式样式 (通过 .dark-mode 类应用) --- */
#quick-search-popup-container.dark-mode {
background-color: rgba(40, 40, 45, 0.75);
background-image: linear-gradient(to bottom right, rgba(50, 50, 55, 0.8), rgba(30, 30, 35, 0.7));
border: 1px solid rgba(100, 100, 100, 0.5);
box-shadow:
0 0 12px 4px rgba(120, 180, 255, 0.25),
0 0 30px 10px rgba(220, 180, 255, 0.2),
0 4px 20px rgba(0, 0, 0, 0.3);
}
#quick-search-popup-container.dark-mode h3 {
color: #eee;
border-bottom-color: rgba(255, 255, 255, 0.15);
}
#quick-search-popup-container.dark-mode #quick-search-input {
background-color: rgba(50, 50, 55, 0.8);
border-color: rgba(100, 100, 100, 0.7);
color: #eee;
}
#quick-search-popup-container.dark-mode #quick-search-input::placeholder {
color: #bbb;
}
#quick-search-popup-container.dark-mode #quick-search-input:focus {
border-color: rgba(100, 170, 255, 0.8);
box-shadow: 0 0 0 2px rgba(100, 170, 255, 0.25);
}
`);
// --- 功能函数 ---
// --- 颜色处理和亮度计算 (与 v1.6 相同) ---
function parseColor(colorString) {
if (!colorString) return null;
let match = colorString.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
if (match) {
return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]), a: match[4] !== undefined ? parseFloat(match[4]) : 1 };
}
match = colorString.match(/^#([a-f0-9]{3}|[a-f0-9]{6})$/i);
if (match) {
let hex = match[1];
if (hex.length === 3) hex = hex.split('').map(char => char + char).join('');
return { r: parseInt(hex.substring(0, 2), 16), g: parseInt(hex.substring(2, 4), 16), b: parseInt(hex.substring(4, 6), 16), a: 1 };
}
if (colorString.toLowerCase() === 'transparent') {
return { r: 0, g: 0, b: 0, a: 0 };
}
return null;
}
function getLuminance(rgb) {
if (!rgb) return 1;
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function isPageDark() {
try {
let element = document.body;
let bgColor = window.getComputedStyle(element).backgroundColor;
let rgb = parseColor(bgColor);
if (rgb && rgb.a < 0.1) {
element = document.documentElement;
bgColor = window.getComputedStyle(element).backgroundColor;
rgb = parseColor(bgColor);
if (rgb && rgb.a < 0.1) return false;
}
if (!rgb) {
console.warn("Quick Search Script: Could not determine page background color.");
return false;
}
const luminance = getLuminance(rgb);
return luminance < DARK_MODE_LUMINANCE_THRESHOLD;
} catch (error) {
console.error("Quick Search Script: Error detecting page darkness:", error);
return false;
}
}
// --- URL 检测函数 ---
/**
* 检查字符串是否像一个 URL
* @param {string} text - 要检查的文本
* @returns {boolean} - 如果像 URL 返回 true
*/
function isLikelyUrl(text) {
if (!text) return false;
text = text.trim();
if (text.includes(' ') || text.length < 4) { // URL 不应包含空格,且长度通常大于3
return false;
}
// 检查是否以常见协议开头
if (/^(https?:\/\/|ftp:\/\/|file:\/\/)/i.test(text)) {
return true;
}
// 检查是否是常见的域名模式 (e.g., example.com, localhost, IP)
// 允许 localhost
if (text.toLowerCase() === 'localhost' || text.startsWith('localhost:')) {
return true;
}
// 允许 IP 地址 (简单检查)
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(text)) {
// 进一步检查 IP 段是否有效 (0-255)
const parts = text.split(':')[0].split('.');
if (parts.length === 4 && parts.every(part => parseInt(part) >= 0 && parseInt(part) <= 255)) {
return true;
}
}
// 检查类域名格式: 包含至少一个点,点前后有字符,顶级域名至少2个字母
if (text.includes('.') && /^[a-z0-9-]+(\.[a-z0-9-]+)+(\:[0-9]+)?(\/.*)?$/i.test(text)) {
// 排除明显不是 URL 的情况,例如文件名 "file.txt"
if (text.includes('.') && !text.endsWith('.') && text.indexOf('.') > 0) {
// 检查顶级域名部分是否至少有两个字母
const parts = text.split('/'); // 移除路径部分
const domainParts = parts[0].split(':'); // 移除端口部分
const hostParts = domainParts[0].split('.');
if (hostParts.length > 1 && hostParts[hostParts.length - 1].length >= 2 && /^[a-z]+$/i.test(hostParts[hostParts.length - 1])) {
return true;
}
}
}
return false;
}
// 创建搜索弹窗 (逻辑不变)
function createPopup() {
if (popupElement) return;
popupElement = document.createElement('div');
popupElement.id = 'quick-search-popup-container';
popupElement.innerHTML = `
<h3>快速搜索</h3>
<input type="text" id="quick-search-input" placeholder="输入或粘贴文本 / 网址">
<div id="quick-search-buttons">
${SEARCH_ENGINES.map(engine => `
<button class="quick-search-button" data-url="${engine.url}" data-name="${engine.name}">${engine.name}</button>
`).join('')}
</div>
`;
document.body.appendChild(popupElement);
popupElement.querySelector('#quick-search-buttons').addEventListener('click', handleSearchButtonClick);
popupElement.querySelector('#quick-search-input').addEventListener('keydown', handleInputKeydown);
document.addEventListener('click', handleClickOutside, true);
}
// 显示弹窗 (逻辑不变)
function showPopup(text) {
if (!popupElement) {
createPopup();
} else {
popupElement.style.transform = 'translate(-50%, -50%) scale(0.85)';
void popupElement.offsetWidth;
}
if (isPageDark()) {
popupElement.classList.add('dark-mode');
} else {
popupElement.classList.remove('dark-mode');
}
const currentSelectedText = (text || "").trim();
const input = popupElement.querySelector('#quick-search-input');
input.value = currentSelectedText;
requestAnimationFrame(() => {
popupElement.classList.add('visible');
popupVisible = true;
});
setTimeout(() => {
input.focus();
if (currentSelectedText) {
input.select();
}
}, 50);
}
// 隐藏弹窗 (逻辑不变)
function hidePopup() {
if (popupElement && popupVisible) {
popupElement.classList.remove('visible');
popupVisible = false;
}
}
// 处理搜索按钮点击 (逻辑不变)
function handleSearchButtonClick(event) {
if (event.target.classList.contains('quick-search-button')) {
const button = event.target;
triggerSearch(button.dataset.name, button.dataset.url);
}
}
// 触发搜索 (逻辑不变)
function triggerSearch(engineName, baseUrl) {
const query = popupElement.querySelector('#quick-search-input').value.trim();
if (query && baseUrl) {
let finalUrl;
if (engineName === '京东') {
finalUrl = baseUrl.replace(/\{query\}/g, encodeURIComponent(query));
} else {
finalUrl = baseUrl + encodeURIComponent(query);
}
GM_openInTab(finalUrl, { active: true });
hidePopup();
} else if (!query) {
const input = popupElement.querySelector('#quick-search-input');
input.focus();
input.placeholder = "请输入搜索内容!";
popupElement.style.transition = 'transform 0.08s ease-in-out';
const baseTransform = popupElement.classList.contains('visible') ? 'translate(-50%, -50%) scale(1)' : 'translate(-50%, -50%) scale(0.85)';
popupElement.style.transform = baseTransform + ' translateX(5px)';
setTimeout(() => {
popupElement.style.transform = baseTransform + ' translateX(-5px)';
setTimeout(() => {
popupElement.style.transform = baseTransform;
setTimeout(() => {
popupElement.style.transition = 'opacity 0.2s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease';
input.placeholder = "输入或粘贴文本 / 网址"; // 更新 placeholder
}, 80);
}, 80);
}, 80);
}
}
// 处理输入框键盘事件 (修改 Enter 逻辑)
function handleInputKeydown(event) {
if (event.key === TRIGGER_KEY) {
event.stopPropagation();
}
if (event.key === 'Enter') {
event.preventDefault(); // 阻止默认行为
const inputText = popupElement.querySelector('#quick-search-input').value.trim();
if (inputText) {
// --- 检查输入是否为 URL ---
if (isLikelyUrl(inputText)) {
let urlToOpen = inputText;
// 如果没有协议,默认添加 http://
if (!/^(https?:\/\/|ftp:\/\/|file:\/\/)/i.test(urlToOpen)) {
urlToOpen = 'http://' + urlToOpen;
}
console.log("Opening URL:", urlToOpen); // 调试信息
GM_openInTab(urlToOpen, { active: true });
hidePopup();
} else {
// --- 如果不是 URL,执行默认搜索 ---
const defaultEngineButton = popupElement.querySelector(`.quick-search-button[data-name="${DEFAULT_SEARCH_ENGINE_NAME}"]`);
if (defaultEngineButton) {
triggerSearch(defaultEngineButton.dataset.name, defaultEngineButton.dataset.url);
} else {
// 备选:使用第一个按钮
const firstButton = popupElement.querySelector('.quick-search-button');
if (firstButton) {
triggerSearch(firstButton.dataset.name, firstButton.dataset.url);
}
}
}
} else {
// 如果输入为空,触发提示 (同 triggerSearch 中的空输入处理)
triggerSearch(null, null); // 调用 triggerSearch 处理空输入提示
}
} else if (event.key === 'Escape') {
hidePopup();
}
}
// 处理点击弹窗外部 (逻辑不变)
function handleClickOutside(event) {
if (popupVisible && popupElement && !popupElement.contains(event.target)) {
if (event instanceof MouseEvent && event.detail > 0) {
hidePopup();
}
}
}
// 处理键盘按下事件 (逻辑不变)
function handleKeyDown(event) {
if (popupVisible && event.key === 'Escape' && document.activeElement !== popupElement.querySelector('#quick-search-input')) {
hidePopup();
return;
}
if (event.key === TRIGGER_KEY && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
if (document.activeElement === popupElement?.querySelector('#quick-search-input')) {
return;
}
const now = Date.now();
if (now - lastKeyPressTime < DOUBLE_PRESS_INTERVAL) {
const currentSelectedText = window.getSelection().toString();
showPopup(currentSelectedText);
lastKeyPressTime = 0;
} else {
lastKeyPressTime = now;
}
}
}
// --- 初始化 ---
document.addEventListener('keydown', handleKeyDown, true);
console.log('Quick Search Script (v1.7 URL Jump+Page Dark Detect) Loaded.');
})();