Greasy Fork

快速搜索选中内容

选中网页文本或双击Alt/Option键弹出搜索框,输入内容按回车默认Google搜索,输入网址按回车直接跳转。根据页面背景适配深/浅色模式。

// ==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.');

})();