Greasy Fork is available in English.

Text Selection Toolbar

Add a text selection toolbar to your browser.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

Advertisement:

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

Advertisement:

// ==UserScript==
// @name               Text Selection Toolbar
// @name:en            Text Selection Toolbar
// @name:ru            Панель_выбора_текста
// @name:zh-CN         划词工具栏
// @namespace          https://github.com/CodebyGPT/Text_Selection_Toolbar
// @version            2026.06.19
// @description        Add a text selection toolbar to your browser.
// @description:en     Add a text selection toolbar to your browser.
// @description:ru     Добавьте панель инструментов для выделения текста в ваш браузер.
// @description:zh-CN  为你的浏览器增加一个划词工具栏。
// @author             CodebyGPT
// @license            GPL-3.0
// @license            https://www.gnu.org/licenses/gpl-3.0.txt
// @match              *://*/*
// @icon               data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjx0aXRsZSB4bWxucz0iIj50b3VjaC10cmlwbGU8L3RpdGxlPjxwYXRoIGZpbGw9ImN1cnJlbnRDb2xvciIgZD0ibTE3Ljk3NSAzLjRsMS0xLjc1cTEuMTc1LjY1IDEuODUgMS44MjVUMjEuNSA2cTAgLjY3NS0uMTc1IDEuMzEzdC0uNSAxLjE4N2wtMS43MjUtMXEuMi0uMzUuMy0uNzEyVDE5LjUgNnEwLS44LS40MTItMS41dC0xLjExMy0xLjFtLTQgMGwxLTEuNzVxMS4xNzUuNjUgMS44NSAxLjgyNVQxNy41IDZxMCAuNjc1LS4xNzUgMS4zMTN0LS41IDEuMTg3bC0xLjcyNS0xcS4yLS4zNS4zLS43MTJUMTUuNSA2cTAtLjgtLjQxMy0xLjV0LTEuMTEyLTEuMW0tMy41IDE4LjZxLS43IDAtMS4zMTItLjN0LTEuMDM4LS44NWwtNS40NS02LjkyNWwuNDc1LS41cS41LS41MjUgMS4yLS42MjV0MS4zLjI3NUw3LjUgMTQuMlY2cTAtLjQyNS4yODgtLjcxMlQ4LjUgNXQuNzI1LjI4OHQuMy43MTJ2NUgxN3ExLjI1IDAgMi4xMjUuODc1VDIwIDE0djRxMCAxLjY1LTEuMTc1IDIuODI1VDE2IDIyem0tNi4zLTEzLjVxLS4zMjUtLjU1LS41LTEuMTg3VDMuNSA2cTAtMi4wNzUgMS40NjMtMy41MzdUOC41IDF0My41MzggMS40NjNUMTMuNSA2cTAgLjY3NS0uMTc1IDEuMzEzdC0uNSAxLjE4N2wtMS43MjUtMXEuMi0uMzUuMy0uNzEyVDExLjUgNnEwLTEuMjUtLjg3NS0yLjEyNVQ4LjUgM3QtMi4xMjUuODc1VDUuNSA2cTAgLjQyNS4xLjc4OHQuMy43MTJ6Ii8+PC9zdmc+
// @grant              GM_registerMenuCommand
// @grant              GM_setValue
// @grant              GM_getValue
// @grant              GM_openInTab
// @grant              GM_addStyle
// @grant              GM_setClipboard
// @sandbox            DOM
// @inject-into        content
// @run-at             document-start
// @supportURL         https://github.com/CodebyGPT/Text_Selection_Toolbar/issues
// ==/UserScript==
const safeGetValue = (key, def) => {
    if (typeof GM !== 'undefined' && GM.getValue) {
        return GM.getValue(key, def);
    } else {
        return Promise.resolve(GM_getValue(key, def));
    }
};

const safeSetValue = (key, val) => {
    if (typeof GM !== 'undefined' && GM.setValue) {
        return GM.setValue(key, val);
    } else {
        return Promise.resolve(GM_setValue(key, val));
    }
};

const safeOpenTab = (url, options) => {
    if (typeof GM !== 'undefined' && GM.openInTab) {
        GM.openInTab(url, options);
    } else {
        GM_openInTab(url, options);
    }
};

const DEFAULT_CONFIG = {
    language: 'auto', // 'auto'(默认) | 'zh-CN' | 'en' | 'ru'
    positionMode: 'endchar', // 'endchar' | 'mouse'
    offset: 12, // px
    timeout: 2400, // ms, 0 = infinite
    buttonStyle: 'row', // 'row' (capsule) | 'col' (rounded rect)
    forceWhiteBlack: true, // true = force white bg/black text
    searchEngine: 'baidu', // key or custom url
    enableToast: true,
    enableCache: true,
    unlockHotkey: 'ControlLeft',
    enablePaste: true,

    enableDragPreview: false,
    scrollRepaintMode: 'always',
    smartEngine: false,        // 是否启用智能分配
    fallbackEngine: 'bing',   // 不含中文时的备用引擎
    enableDeleteBtn: true, // 是否显示删除按钮
    customTLDs: [], // 用户自定义的顶级域名列表
};

const SCROLL_REPAINT_MODE = {
    ALWAYS: 'always',      // 1. 始终重绘(默认)
    VIEWPORT: 'viewport',  // 2. 锚点在视口内才重绘
    HIDE: 'hide'           // 3. 滚动即隐藏,不重绘
};

const PASTE_MODE_THREE_BTNS = 'copy-search-paste';   // 闪电粘贴三按钮模式标记

const SEARCH_ENGINES = {
    google: { name: 'Google', url: 'https://www.google.com/search?q=%s' },
    baidu: { name: 'Baidu', url: 'https://www.baidu.com/s?wd=%s' },
    bing: { name: 'Bing', url: 'https://www.bing.com/search?q=%s' },
    brave: { name: 'Brave', url: 'https://search.brave.com/search?q=%s' },
};

const TLD_SET = new Set([
    'com', 'cn', 'de', 'tk', 'uk', 'net', 'org', 'top', 'ru',
    'info', 'br', 'xyz', 'ga', 'nl', 'it', 'ws', 'ml', 'shop',
    'cf', 'fr', 'co', 'eu', 'in', 'online', 'au', 'gq', 'ph',
    'us', 'ca', 'vip', 'club', 'pl', 'cc', 'biz', 'store', 'za',
    'site', 'ch', 'se', 'es', 'tw', 'loan', 'jp', 'me', 'be',
    'live', 'buzz', 'at', 'ir', 'work', 'app', 'sbs', 'cz', 'pro',
    'click', 'id', 'dk', 'io', 'mx', 'bond', 'kr', 'wang', 'lol',
    'no', 'tr', 'cfd', 'nu', 'hu', 'life', 'ai', 'asia', 'my',
    'cl', 'ua', 'ro', 'icu', 'cloud', 'win', 'link', 'ar', 'nz',
    'vn', 'ltd', 'world', 'dev', 'fun', 'mobi', 'space', 'tv',
    'cyou', 'fi', 'tech', 'sk', 'today', 'gr', 'one', 'digital',
    'gov', 'edu'
]);
const TLD_SET_EXTENDED = new Set(TLD_SET); // 可扩展副本,用于合并自定义TLD

let cachedSelection = { text: '', html: '' };
let uiTimer = null;
let toastTimer = null;
let isScrolling = false;
let scrollTimeout = null;
let shadowRoot = null;
let hostElement = null;

let configCache = { ...DEFAULT_CONFIG };

const getConfig = (key) => {
    return configCache[key];
};

const setConfig = async (key, val) => {
    configCache[key] = val; // 立即更新内存,保证交互响应
    await safeSetValue(key, val); // 异步写入持久化存储
};

const I18N = {
    'zh-CN': {
        lang_name: '简体中文',
        menu_lang: '🌐 语言/Language',
        menu_pos: '📍 UI 弹出位置',
        val_endchar: '字符末尾',
        val_mouse: '光标附近',
        menu_offset: '📏 UI 弹出偏移量',
        prompt_offset: '请输入 UI 距离锚点的偏移量 (px):',
        menu_timeout: '⏱️ UI 停留时长',
        val_infinite: '不消失',
        prompt_timeout: '请输入 UI 停留时长 (ms, 0表示不自动消失):',
        menu_style: '🎨 UI 布局',
        val_row: '横排胶囊',
        val_col: '纵排矩形',
        menu_theme: '🌓 UI 配色',
        val_light: '强制浅色',
        val_auto: '自动反色',
        menu_search: '🔍 搜索引擎',
        prompt_search: '请输入搜索引擎代码 (google, baidu, bing, brave) 或完整URL (%s 代替关键词):',
        err_search: '无效的输入。自定义URL需包含 %s',
        menu_cache: '💾 选中即缓存',
        val_on: '开启',
        val_off: '关闭',
        menu_toast: '🔔 操作反馈',
        menu_hotkey: '🔑 超级取词键',
        val_disabled: '已禁用',
        prompt_hotkey: '请指定快捷键 (如 Ctrl, Alt, Shift) 或输入 "NONE" 以禁用:',
        menu_paste: '⚡ 闪电粘贴',
        menu_block: '🚫 屏蔽网页自建划词栏',
        menu_clear: '🗑️ 清除当前域名屏蔽规则',
        confirm_clear: '确定要清除 %s 下所有屏蔽规则吗?',
        alert_cleared: '规则已清除,请刷新。',
        alert_no_rules: '当前域名无已保存的规则。',
        menu_reset: '⚙️ 重置全部设置',
        confirm_reset: '确定要重置所有的设置吗?',
        toast_unlock: '🔓 超级取词已激活',
        toast_copied: '已复制',
        toast_pasted: '已粘贴',
        toast_paste_compat: '已粘贴 (兼容模式)',
        toast_paste_fail: '粘贴失败',
        picker_active: '已进入拾取模式;按 ESC 退出',
        picker_cant_block_self: '不能屏蔽脚本自身的按钮!',
        picker_confirm: '确定屏蔽该元素吗?(按Esc退出)\n\n选择器: %s',
        picker_saved: '元素已屏蔽并保存规则',
        picker_exit: '已退出拾取模式',
        btn_copy: '复制',
        btn_search: '搜索',
        btn_paste: '粘贴',
        festival_cny: '🏮已复制🏮',
        festival_xmas: '🎄已复制🎄',
        btn_open_link: '打开链接',
        btn_email: '@ 复制邮箱',
        toast_email_copied: '邮箱地址已复制',
        toast_password_pasted: '已粘贴提取码',
        menu_tld_add: '➕ 添加自定义顶级域名',
        prompt_tld_add: '请输入要添加的顶级域名 (如 xyz 或 .xyz):',
        toast_tld_added: '已添加域名: %s',
        err_tld_invalid: '无效的域名格式。请输入如 xyz 或 .xyz',
        menu_drag_preview: '🔗 拖拽预览',
        btn_cut: '剪切',
        menu_edit: '✏️ 编辑网页',
        menu_exit_edit: '已退出编辑',
        btn_delete: '删除',
        btn_bold: '加粗',
        btn_highlight: '标记',
        disclaimer_text: '此网页内容已经过 <SCRIPT_NAME> 编辑',
        scroll_repaint: '📜 UI 重绘策略',
        scroll_always: '始终重绘',
        scroll_viewport: '锚点在视口内重绘',
        scroll_hide: '始终不重绘',
        menu_smart_engine: '🧠 智能分配搜索引擎',
        menu_fallback_engine: '🔍 备用搜索引擎',
        val_smart_on: '开启',
        val_smart_off: '关闭',
        menu_delete_btn: '🗑️ 删除按钮可见性',
        val_show: '显示',
        val_hide: '隐藏',
    },
    'en': {
        lang_name: 'English',
        menu_lang: '🌐 Language',
        menu_pos: '📍 Position',
        val_endchar: 'End of Text',
        val_mouse: 'Mouse Cursor',
        menu_offset: '📏 Offset',
        prompt_offset: 'Enter offset distance (px):',
        menu_timeout: '⏱️ Timeout',
        val_infinite: 'Infinite',
        prompt_timeout: 'Enter timeout (ms, 0 = infinite):',
        menu_style: '🎨 Layout',
        val_row: 'Row (Capsule)',
        val_col: 'Column (Rect)',
        menu_theme: '🌓 Theme',
        val_light: 'Force Light',
        val_auto: 'Auto Contrast',
        menu_search: '🔍 Engine',
        prompt_search: 'Enter engine code (google, bing...) or URL with %s:',
        err_search: 'Invalid input. Custom URL must contain %s',
        menu_cache: '💾 Cache Selection',
        val_on: 'On',
        val_off: 'Off',
        menu_toast: '🔔 Toast Notification',
        menu_hotkey: '🔑 Unlock Hotkey',
        val_disabled: 'Disabled',
        prompt_hotkey: 'Press a key (Ctrl, Alt...) or type "NONE" to disable:',
        menu_paste: '⚡ Smart Paste',
        menu_block: '🚫 Block Page Element',
        menu_clear: '🗑️ Clear Block Rules',
        confirm_clear: 'Clear all rules for %s?',
        alert_cleared: 'Rules cleared. Please refresh.',
        alert_no_rules: 'No rules found for this domain.',
        menu_reset: '⚙️ Reset Settings',
        confirm_reset: 'Reset all settings?',
        toast_unlock: '🔓 Unlock Mode Active',
        toast_copied: 'Copied',
        toast_pasted: 'Pasted',
        toast_paste_compat: 'Pasted (Compat)',
        toast_paste_fail: 'Paste Failed',
        picker_active: 'Picker Mode Active (ESC to exit)',
        picker_cant_block_self: 'Cannot block script UI!',
        picker_confirm: 'Block this element? (ESC to cancel)\n\nSelector: %s',
        picker_saved: 'Element blocked & saved.',
        picker_exit: 'Picker Mode Exited',
        btn_copy: 'Copy',
        btn_search: 'Search',
        btn_paste: 'Paste',
        festival_cny: '🏮 Copied 🏮',
        festival_xmas: '🎄 Copied 🎄',
        btn_open_link: 'Open Link',
        btn_email: '@ Copy Email',
        toast_email_copied: 'Email Copied',
        toast_password_pasted: 'Code Pasted',
        menu_tld_add: '➕ Add Custom TLD',
        prompt_tld_add: 'Enter TLD to add (e.g. xyz or .xyz):',
        toast_tld_added: 'TLD added: %s',
        err_tld_invalid: 'Invalid TLD format. Use e.g. xyz or .xyz',
        menu_drag_preview: '🔗 Drag Link Preview',
        btn_cut: 'Cut',
        menu_edit: '✏️ Edit Page',
        menu_exit_edit: 'Exit Edit Mode',
        btn_delete: 'Delete',
        btn_bold: 'Bold',
        btn_highlight: 'Highlight',
        disclaimer_text: 'Content edited by <SCRIPT_NAME> for simplification purposes only.',
        scroll_repaint: '📜 UI redrawing',
        scroll_always: 'Always redraw',
        scroll_viewport: 'Redraw anchor points within the viewport',
        scroll_hide: 'Never redraw',
        menu_smart_engine: '🧠 Smart Engine',
        menu_fallback_engine: '🔍 Fallback Engine',
        val_smart_on: 'On',
        val_smart_off: 'Off',
        menu_delete_btn: '🗑️ Visibility of the delete button',
        val_show: 'Show',
        val_hide: 'Hide',
    },
    'ru': {
        lang_name: 'Русский',
        menu_lang: '🌐 Язык/Language',
        menu_pos: '📍 Позиция',
        val_endchar: 'Конец текста',
        val_mouse: 'Курсор мыши',
        menu_offset: '📏 Отступ',
        prompt_offset: 'Введите отступ (px):',
        menu_timeout: '⏱️ Задержка',
        val_infinite: 'Бесконечно',
        prompt_timeout: 'Введите задержку (мс, 0 = бесконечно):',
        menu_style: '🎨 Стиль кнопок',
        val_row: 'Строка',
        val_col: 'Колонка',
        menu_theme: '🌓 Тема',
        val_light: 'Светлая',
        val_auto: 'Авто',
        menu_search: '🔍 Поисковик',
        prompt_search: 'Код (google, yandex...) или URL с %s:',
        err_search: 'Ошибка. URL должен содержать %s',
        menu_cache: '💾 Кэш выделения',
        val_on: 'Вкл',
        val_off: 'Выкл',
        menu_toast: '🔔 Уведомления',
        menu_hotkey: '🔑 Горячая клавиша',
        val_disabled: 'Откл',
        prompt_hotkey: 'Нажмите клавишу (Ctrl, Alt...) или "NONE":',
        menu_paste: '⚡ Быстрая вставка',
        menu_block: '🚫 Блокировка элементов',
        menu_clear: '🗑️ Сброс блокировок',
        confirm_clear: 'Удалить правила для %s?',
        alert_cleared: 'Правила удалены. Обновите страницу.',
        alert_no_rules: 'Нет правил для этого домена.',
        menu_reset: '⚙️ Сброс настроек',
        confirm_reset: 'Сбросить все настройки?',
        toast_unlock: '🔓 Режим разблокировки',
        toast_copied: 'Скопировано',
        toast_pasted: 'Вставлено',
        toast_paste_compat: 'Вставлено (совм.)',
        toast_paste_fail: 'Ошибка вставки',
        picker_active: 'Режим выбора (ESC для выхода)',
        picker_cant_block_self: 'Нельзя блокировать кнопки скрипта!',
        picker_confirm: 'Блокировать элемент? (ESC - отмена)\n\nСелектор: %s',
        picker_saved: 'Заблокировано и сохранено.',
        picker_exit: 'Режим выбора отключен',
        btn_copy: 'Копировать',
        btn_search: 'Поиск',
        btn_paste: 'Вставить',
        festival_cny: '🏮 Скопировано 🏮',
        festival_xmas: '🎄 Скопировано 🎄',
        btn_open_link: 'Открыть ссылку',
        btn_email: '@ Копировать Email',
        toast_email_copied: 'Email скопирован',
        toast_password_pasted: 'Код вставлен',
        menu_tld_add: '➕ Добавить свой TLD',
        prompt_tld_add: 'Введите TLD (например xyz или .xyz):',
        toast_tld_added: 'TLD добавлен: %s',
        err_tld_invalid: 'Неверный формат TLD. Используйте напр. xyz или .xyz',
        menu_drag_preview: '🔗 Предпросмотр ссылки',
        btn_cut: 'Вырезать',
        menu_edit: '✏️ Редактировать',
        menu_exit_edit: 'Выход из редактора',
        btn_delete: 'Удалить',
        btn_bold: 'Жирный',
        btn_highlight: 'Маркер',
        disclaimer_text: 'Контент отредактирован <SCRIPT_NAME> только для упрощения просмотра.',
        scroll_repaint: '📜 Перерисовка интерфейса',
        scroll_always: 'Всегда перерисовывать',
        scroll_viewport: 'Анкор перерисовывается внутри окна просмотра',
        scroll_hide: 'Всегда не перерисовывать',
        menu_smart_engine: '🧠 Умный поиск',
        menu_fallback_engine: '🔍 Резерв поиск',
        val_smart_on: 'Вкл',
        val_smart_off: 'Выкл',
        menu_delete_btn: '🗑️ видимость кнопки удаления',
        val_show: 'Показать',
        val_hide: 'Скрыть',
    }
};

const t = (key, ...args) => {
    let lang = getConfig('language');
    if (lang === 'auto') {
        const nav = navigator.language.toLowerCase();
        if (nav.startsWith('zh')) lang = 'zh-CN';
        else if (nav.startsWith('ru')) lang = 'ru';
        else lang = 'en';
    }
    const dict = I18N[lang] || I18N['en'];
    let str = dict[key] || key;
    args.forEach(arg => str = str.replace('%s', arg));
    return str;
};

let isEditMode = false;
let hasEditSessionStarted = false; // 标记本次会话是否启用过编辑模式
let complianceObserver = null;
let currentBannerId = null;

const generateRandomId = () => 'tm-sc-' + Math.random().toString(36).slice(2, 9);

function ensureComplianceBanner() {
    if (!hasEditSessionStarted) return; // 如果从未启动过编辑模式,不生成

    const existing = currentBannerId ? document.getElementById(currentBannerId) : null;
    if (existing && existing.offsetParent !== null) return;// 如果存在且看起来正常(display不是none),则跳过
    if (existing) existing.remove();// 如果存在但被隐藏了,或者不存在,则继续重建逻辑

    if (complianceObserver) {
        complianceObserver.disconnect();
    }

    const scriptName = GM_info.script.name;
    const banner = document.createElement('div');
    currentBannerId = generateRandomId();
    banner.id = currentBannerId;

    banner.setAttribute('data-tm-policy', 'protected'); // [关键] 添加特殊策略标记,用于 CSS 排除
    banner.setAttribute('contenteditable', 'false');

    banner.style.cssText = `
        position: fixed !important;
        bottom: 50px !important;
        left: 50% !important;
        transform: translateX(-50%) !important;
        z-index: 2147483647 !important;
        background: rgba(255, 255, 255, 0.85) !important;
        padding: 6px 14px !important;
        border-radius: 6px !important;
        box-shadow: 0 2px 10px rgba(0,0,0,0.08) !important;
        pointer-events: none !important; /* 让鼠标穿透,既不影响浏览,也防止被拾取器选中 */
        user-select: none !important;
        -webkit-user-select: none !important;
        display: flex !important;
        align-items: center !important;
        gap: 8px !important;
        visibility: visible !important;
        opacity: 1 !important;
        width: auto !important;
        height: auto !important;
        border: 1px solid rgba(0,0,0,0.05) !important;
    `;

    const iconContainer = document.createElement('div');
    iconContainer.style.cssText = 'display:flex;align-items:center;color:#888;pointer-events:none;';
    iconContainer.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`;
    banner.appendChild(iconContainer);

    const textStr = t('disclaimer_text').replace('<SCRIPT_NAME>', scriptName);
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const fontSize = 12;
    const fontFamily = 'sans-serif';

    ctx.font = `${fontSize}px ${fontFamily}`;
    const metrics = ctx.measureText(textStr);
    const textWidth = Math.ceil(metrics.width);
    const textHeight = Math.ceil(fontSize * 1.2); // 留一点行高

    const dpr = window.devicePixelRatio || 1;
    canvas.width = textWidth * dpr;
    canvas.height = textHeight * dpr;
    canvas.style.width = `${textWidth}px`;
    canvas.style.height = `${textHeight}px`;
    canvas.style.pointerEvents = 'none';

    ctx.scale(dpr, dpr);
    ctx.font = `${fontSize}px ${fontFamily}`;
    ctx.fillStyle = '#999';
    ctx.textBaseline = 'middle';
    ctx.fillText(textStr, 0, textHeight / 2 + 1); // +1 微调垂直居中

    banner.appendChild(canvas);
    document.body.appendChild(banner);

    complianceObserver = new MutationObserver((mutations) => {
        let needsRebuild = false;
        mutations.forEach(m => {
            if (m.removedNodes.length) {
                m.removedNodes.forEach(node => {
                    if (node.id === currentBannerId) needsRebuild = true;
                });
            }
            if (m.target.id === currentBannerId) {
                needsRebuild = true;
            }
            if (m.target.id === currentBannerId && m.type === 'childList') needsRebuild = true;
        });

        if (needsRebuild) { // 异步重建防止死锁
            setTimeout(() => { // 使用 setTimeout 避免在Observer回调中同步操作DOM
                const old = document.getElementById(currentBannerId); // 销毁旧的引用(如果还在DOM里但被改了)
                if (old) old.remove();
                ensureComplianceBanner();
            }, 0);
        }
    });

    complianceObserver.observe(document.body, { childList: true, subtree: false }); // 监控 body 子节点删除
    setTimeout(() => { // 注意:这里需要再次获取最新的 banner 引用
        const b = document.getElementById(currentBannerId);
        if (b && complianceObserver) {
            complianceObserver.observe(b, { attributes: true, attributeFilter: ['style', 'class', 'hidden', 'id', 'data-tm-policy', 'contenteditable'], childList: true, subtree: true });
        }
    }, 0);
}

function toggleEditMode(enable) {
    if (isEditMode === enable) return;
    isEditMode = enable;

    if (isEditMode) {
        hasEditSessionStarted = true; // 标记会话已开始,此后 Banner 即使退出编辑模式也会常驻
        document.designMode = 'on';
        ensureComplianceBanner();
        showToast(t('menu_edit') + ': ' + t('val_on'));
    } else {
        document.designMode = 'off';
        showToast(t('menu_exit_edit'));
        hideUI(); // 隐藏可能残留的按钮

        ensureComplianceBanner();  // 确保 Banner 依然存在 (防止在切换瞬间被误删)
    }
}

async function initConfiguration() {
    configCache['scrollRepaintMode'] = await safeGetValue('scrollRepaintMode', 'always');
    const keys = Object.keys(DEFAULT_CONFIG);
    const values = await Promise.all(
        keys.map(key => safeGetValue(key, DEFAULT_CONFIG[key]))
    );

    keys.forEach((key, index) => {
        configCache[key] = values[index];
    });

    const blockedRules = await safeGetValue('blocked_elements', {});
    configCache['blocked_elements'] = blockedRules;

    const customTLDs = configCache['customTLDs'] || [];
    if (customTLDs.length > 0) {
        customTLDs.forEach(t => TLD_SET_EXTENDED.add(t.toLowerCase().replace(/^\./, '')));
    }
}

async function initDefaultSearchEngine() {
    const hasInitialized = await safeGetValue('engine_initialized', false);
    if (!hasInitialized) {
        const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
        const chinaTimeZones = ['Asia/Shanghai', 'Asia/Urumqi'];
        const defaultEngine = chinaTimeZones.includes(timeZone) ? 'baidu' : 'google';

        configCache['searchEngine'] = defaultEngine;
        await safeSetValue('searchEngine', defaultEngine);
        await safeSetValue('engine_initialized', true);
    }
}

function registerMenus() {
    const curLang = getConfig('language');
    const langLabel = curLang === 'auto' ? 'Auto' : (I18N[curLang] ? I18N[curLang].lang_name : curLang);
    GM_registerMenuCommand(`${t('menu_lang')}: ${langLabel}`, () => {
        const nextMap = { 'auto': 'zh-CN', 'zh-CN': 'en', 'en': 'ru', 'ru': 'auto' };
        setConfig('language', nextMap[curLang] || 'auto');
        location.reload();
    });

    const posMode = getConfig('positionMode');
    GM_registerMenuCommand(`${t('menu_pos')}: ${posMode === 'endchar' ? t('val_endchar') : t('val_mouse')}`, () => {
        setConfig('positionMode', posMode === 'endchar' ? 'mouse' : 'endchar');
        location.reload();
    });

    GM_registerMenuCommand(`${t('menu_offset')}: ${getConfig('offset')}px`, () => {
        const val = prompt(t('prompt_offset'), getConfig('offset'));
        if (val !== null && !isNaN(val)) {
            setConfig('offset', parseInt(val, 10));
            location.reload();
        }
    });

    const scrollMode = getConfig('scrollRepaintMode');
    const modeText = {
        always: t('scroll_always'),
        viewport: t('scroll_viewport'),
        hide: t('scroll_hide')
    };
    GM_registerMenuCommand(`${t('scroll_repaint')}: ${modeText[scrollMode]}`, () => {
        const nextMap = { always: 'viewport', viewport: 'hide', hide: 'always' };
        setConfig('scrollRepaintMode', nextMap[scrollMode] || 'always');
        location.reload();
    });

    const timeout = getConfig('timeout');
    GM_registerMenuCommand(`${t('menu_timeout')}: ${timeout === 0 ? t('val_infinite') : timeout + 'ms'}`, () => {
        const val = prompt(t('prompt_timeout'), timeout);
        if (val !== null && !isNaN(val)) {
            setConfig('timeout', parseInt(val, 10));
            location.reload();
        }
    });

    const btnStyle = getConfig('buttonStyle');
    GM_registerMenuCommand(`${t('menu_style')}: ${btnStyle === 'row' ? t('val_row') : t('val_col')}`, () => {
        setConfig('buttonStyle', btnStyle === 'row' ? 'col' : 'row');
        location.reload();
    });

    const forceWB = getConfig('forceWhiteBlack');
    GM_registerMenuCommand(`${t('menu_theme')}: ${forceWB ? t('val_light') : t('val_auto')}`, () => {
        setConfig('forceWhiteBlack', !forceWB);
        location.reload();
    });

    const showDelete = getConfig('enableDeleteBtn');
    GM_registerMenuCommand(`${t('menu_delete_btn')}: ${showDelete ? t('val_show') : t('val_hide')}`, () => {
        setConfig('enableDeleteBtn', !showDelete);
        location.reload();
    });

    const currentEngineKey = getConfig('searchEngine');
    const engineName = SEARCH_ENGINES[currentEngineKey] ? SEARCH_ENGINES[currentEngineKey].name : 'Custom';
    GM_registerMenuCommand(`${t('menu_search')}: ${engineName}`, () => {
        const choice = prompt(t('prompt_search'), currentEngineKey);
        if (choice) {
            if (SEARCH_ENGINES[choice] || choice.includes('%s')) {
                setConfig('searchEngine', choice);
                location.reload();
            } else {
                alert(t('err_search'));
            }
        }
    });

    const smartOn = getConfig('smartEngine');
    GM_registerMenuCommand(`${t('menu_smart_engine')}: ${smartOn ? t('val_smart_on') : t('val_smart_off')}`, () => {
        setConfig('smartEngine', !smartOn);
        location.reload();
    });

    if (smartOn) {
        const fbKey = getConfig('fallbackEngine');
        const fbName = SEARCH_ENGINES[fbKey] ? SEARCH_ENGINES[fbKey].name : 'Custom';
        GM_registerMenuCommand(`${t('menu_fallback_engine')}: ${fbName}`, () => {
            const choice = prompt(t('prompt_search'), fbKey);
            if (choice) {
                if (SEARCH_ENGINES[choice] || choice.includes('%s')) {
                    setConfig('fallbackEngine', choice);
                    location.reload();
                } else {
                    alert(t('err_search'));
                }
            }
        });
    }

    GM_registerMenuCommand(`${t('menu_cache')}: ${getConfig('enableCache') ? t('val_on') : t('val_off')}`, () => {
        setConfig('enableCache', !getConfig('enableCache'));
        location.reload();
    });

    GM_registerMenuCommand(`${t('menu_toast')}: ${getConfig('enableToast') ? t('val_on') : t('val_off')}`, () => {
        setConfig('enableToast', !getConfig('enableToast'));
        location.reload();
    });

    const currentKey = getConfig('unlockHotkey');
    GM_registerMenuCommand(`${t('menu_hotkey')}: ${currentKey || t('val_disabled')}`, () => {
        const val = prompt(t('prompt_hotkey'));
        if (val === null) return;

        let finalKey = val.trim();
        if (finalKey.toLowerCase() === 'ctrl') finalKey = 'ControlLeft';
        if (finalKey.toLowerCase() === 'alt') finalKey = 'AltLeft';
        if (finalKey.toLowerCase() === 'shift') finalKey = 'ShiftLeft';
        if (finalKey === '' || finalKey.toUpperCase() === 'NONE') finalKey = '';

        setConfig('unlockHotkey', finalKey);
        location.reload();
    });

    GM_registerMenuCommand(`${t('menu_paste')}: ${getConfig('enablePaste') ? t('val_on') : t('val_off')}`, () => {
        setConfig('enablePaste', !getConfig('enablePaste'));
        location.reload();
    });

    GM_registerMenuCommand(`${t('menu_drag_preview')}: ${getConfig('enableDragPreview') ? t('val_on') : t('val_off')}`, () => {
        setConfig('enableDragPreview', !getConfig('enableDragPreview'));
        location.reload();
    });

    GM_registerMenuCommand(t('menu_block'), () => {
        activateElementPicker();
    });

    GM_registerMenuCommand(t('menu_clear'), async () => {
        const domain = location.hostname;
        if (confirm(t('confirm_clear', domain))) {
            const rules = await safeGetValue('blocked_elements', {});
            if (rules[domain]) {
                delete rules[domain];
                await safeSetValue('blocked_elements', rules);
                if (typeof configCache !== 'undefined') { configCache['blocked_elements'] = rules; }
                alert(t('alert_cleared'));
                location.reload();
            } else {
                alert(t('alert_no_rules'));
            }
        }
    });

    GM_registerMenuCommand(t('menu_tld_add'), () => {
        const val = prompt(t('prompt_tld_add'));
        if (!val) return;
        let tld = val.trim().toLowerCase().replace(/^\./, ''); // 移除前导点
        if (!/^[a-z]{2,}$/.test(tld)) {
            alert(t('err_tld_invalid'));
            return;
        }
        const current = getConfig('customTLDs') || [];
        if (current.includes(tld)) {
            showToast('TLD already exists: ' + tld);
            return;
        }
        current.push(tld);
        setConfig('customTLDs', current);
        TLD_SET_EXTENDED.add(tld);
        showToast(t('toast_tld_added', tld));
    });

    GM_registerMenuCommand(t('menu_edit'), () => {
        toggleEditMode(!isEditMode);
    });

    GM_registerMenuCommand(t('menu_reset'), async () => {
        if (confirm(t('confirm_reset'))) {
            const keys = Object.keys(DEFAULT_CONFIG);
            await Promise.all(keys.map(k => setConfig(k, DEFAULT_CONFIG[k])));
            location.reload();
        }
    });
}

function getEffectiveTLDs() {
    const custom = getConfig('customTLDs') || [];
    if (custom.length === 0) return TLD_SET_EXTENDED;
    const merged = new Set(TLD_SET_EXTENDED);
    custom.forEach(t => merged.add(t.toLowerCase().replace(/^\./, '')));
    return merged;
}

const isUrlSafeChar = (ch) => {
    const code = ch.charCodeAt(0);
    return code < 128 && /^[a-zA-Z0-9._~:/?#[\]@!$&'()*+,;=%-]$/.test(ch);
};

const isChineseChar = (ch) => /[\u4e00-\u9fa5]/.test(ch);

function trimUrlTail(url) {
    url = url.replace(/[,.。;:!?!?、]+$/, '');
    const openParens = (url.match(/\(/g) || []).length;
    const closeParens = (url.match(/\)/g) || []).length;
    if (closeParens > openParens) {
        const excess = closeParens - openParens;
        for (let i = 0; i < excess; i++) {
            url = url.replace(/\)+$/, (m) => m.slice(1));
        }
    }
    if (openParens > closeParens && url.endsWith('(')) {
        url = url.slice(0, -1);
    }
    url = url.replace(/[,.。;:!?!?、]+$/, '');
    return url;
}

function scanUrlPath(text, startPos) {
    let urlEnd = startPos;
    let sawChinese = false;

    for (let i = startPos; i < text.length; i++) {
        const ch = text[i];

        if (isChineseChar(ch)) {
            sawChinese = true;
            const collected = text.substring(startPos, urlEnd);
            if (collected.endsWith('(:') || collected.endsWith('(')) {
                while (urlEnd > startPos && text[urlEnd - 1] !== '(') {
                    urlEnd--;
                }
                urlEnd--;
            }
            continue;
        }

        if (sawChinese) break;

        if (isUrlSafeChar(ch)) {
            urlEnd = i + 1;
        } else {
            break;
        }
    }

    return urlEnd;
}

const PROTO_ANCHOR_PATTERN = /https?:\/\/[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}/gi;

const DOMAIN_ANCHOR_PATTERN = /(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}/gi;

function extractUrlsFromText(text) {
    const effectiveTLDs = getEffectiveTLDs();
    const results = [];

    const allAnchors = [];

    let m;
    const protoRegex = new RegExp(PROTO_ANCHOR_PATTERN.source, 'gi');
    while ((m = protoRegex.exec(text)) !== null) {
        allAnchors.push({
            start: m.index,
            end: m.index + m[0].length,
            hostAndProto: m[0],
            hasProto: true
        });
    }

    const domainRegex = new RegExp(DOMAIN_ANCHOR_PATTERN.source, 'gi');
    while ((m = domainRegex.exec(text)) !== null) {
        const isOverlapped = allAnchors.some(a =>
            m.index >= a.start && m.index < a.end
        );
        if (!isOverlapped) {
            allAnchors.push({
                start: m.index,
                end: m.index + m[0].length,
                hostAndProto: m[0],
                hasProto: false
            });
        }
    }

    allAnchors.sort((a, b) => a.start - b.start);

    const deduped = [];
    for (const anchor of allAnchors) {
        if (deduped.length === 0 || anchor.start >= deduped[deduped.length - 1].end) {
            deduped.push(anchor);
        }
    }

    let lastUrlEnd = 0;
    for (const anchor of deduped) {
        if (anchor.start < lastUrlEnd) continue;
        const host = anchor.hostAndProto.replace(/^https?:\/\//, '').split('/')[0];
        const tld = host.split('.').pop().toLowerCase();
        if (!effectiveTLDs.has(tld)) continue;
        if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.)/.test(host)) continue;

        const pathEnd = scanUrlPath(text, anchor.end);
        let url = text.substring(anchor.start, pathEnd);
        url = url.replace(/[\u4e00-\u9fa5]+/g, '');
        url = trimUrlTail(url);

        if (!anchor.hasProto) {
            const urlHost = url.split('/')[0];
            if (url === urlHost || url.length <= urlHost.length) {
                const questionIdx = text.indexOf('?', anchor.end);
                if (questionIdx !== -1 && questionIdx < anchor.end + 50) {
                }
                if (!/[/?#]/.test(url)) continue;
            }
        }

        const finalHost = url.replace(/^https?:\/\//, '').split('/')[0];
        const finalTld = finalHost.split('.').pop().toLowerCase();
        if (!effectiveTLDs.has(finalTld)) continue;

        const fullUrl = url.startsWith('http') ? url : 'http://' + url;
        const displayUrl = anchor.hasProto ? url : url; // display shows with http:// added

        results.push({
            display: fullUrl.replace(/^https?:\/\//, '') === url.replace(/^https?:\/\//, '')
                ? url : fullUrl,
            url: fullUrl,
            host: finalHost,
            anchorStart: anchor.start  // 保留锚点在原文中的位置,用于密码分配
        });

        lastUrlEnd = pathEnd;
    }

    return results;
}

function extractAllCodesWithPositions(rawText) {
    const results = [];
    const codePatterns = [
        /(?:提取码|提取密碼|密码|訪問碼|访问码|分享码|口令|code|pwd|key|pw|pass)\s*[::\s]+\s*([a-zA-Z0-9]{3,8})(?![a-zA-Z0-9])/gi,
        /(?:提取码|提取密碼|密码|訪問碼|访问码|分享码|口令|code|pwd|key|pw|pass)[::]([a-zA-Z0-9]{3,8})(?![a-zA-Z0-9])/gi,
        /码\s*[::\s]*([a-zA-Z0-9]{3,8})(?![a-zA-Z0-9])/gi,
        /\([::\s]*([a-zA-Z0-9]{3,8})\s*\)/gi,
    ];
    const seen = new Set(); // 去重:同一位置同一code只记一次
    for (const pat of codePatterns) {
        let m;
        while ((m = pat.exec(rawText)) !== null) {
            const key = m.index + '|' + m[1];
            if (!seen.has(key)) {
                seen.add(key);
                results.push({ code: m[1], index: m.index });
            }
        }
    }
    results.sort((a, b) => a.index - b.index);
    return results;
}

function extractLinkAndCode(rawText) {
    if (!rawText) return null;

    const allCodes = extractAllCodesWithPositions(rawText);
    const password = allCodes.length > 0 ? allCodes[0].code : null;

    let urls = extractUrlsFromText(rawText);

    if (urls.length === 0) {
        const cleanText = rawText
            .replace(/[\u4e00-\u9fa5]+/g, '')
            .replace(/\s+/g, '');
        urls = extractUrlsFromText(cleanText);
    }

    if (urls.length === 0 && !password) return null;

    for (let i = 0; i < urls.length; i++) {
        const urlStart = urls[i].anchorStart;
        if (urlStart === undefined) continue;
        const nextUrlStart = (i + 1 < urls.length && urls[i + 1].anchorStart !== undefined)
            ? urls[i + 1].anchorStart
            : rawText.length;
        const matched = allCodes.filter(c => c.index >= urlStart && c.index < nextUrlStart);
        if (matched.length > 0) {
            urls[i].code = matched[0].code;
        }
    }

    return {
        urls: urls,
        password: password
    };
}

function extractEmailFromText(rawText) {
    if (!rawText || !rawText.includes('@')) return null;
    const emailPattern = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/;
    const m = rawText.match(emailPattern);
    return m ? m[1] : null;
}

function getSmartSelectionState(selection, mouseEvent) {
    if (!selection || selection.rangeCount === 0) return null;

    const range = selection.getRangeAt(0);
    let rects = range.getClientRects();

    let targetRect = null;
    let isBackward = false;
    let isVertical = false;

    if (rects.length > 0) {
        const anchor = selection.anchorNode;
        const focus = selection.focusNode;

        if (anchor === focus) {
            isBackward = selection.anchorOffset > selection.focusOffset;
        } else {
            const pos = anchor.compareDocumentPosition(focus);
            if (pos & Node.DOCUMENT_POSITION_PRECEDING) isBackward = true;
        }

        let focusEl = focus.nodeType === 1 ? focus : focus.parentElement;
        if (focusEl) {
            const style = window.getComputedStyle(focusEl);
            const writingMode = style.writingMode || 'horizontal-tb';
            isVertical = writingMode.startsWith('vertical');
        }

        targetRect = isBackward ? rects[0] : rects[rects.length - 1];
    }

    const isInvalidRect = (r) => {
        return !r || (r.width === 0 && r.height === 0 && r.top === 0 && r.left === 0);
    };

    if (isInvalidRect(targetRect)) {
        const bounding = range.getBoundingClientRect();
        if (!isInvalidRect(bounding)) {
            targetRect = bounding;
            isBackward = false;
            isVertical = false;
        }
    }

    if (isInvalidRect(targetRect) && mouseEvent) {
        const size = 20; // 模拟一个光标高度
        targetRect = {
            top: mouseEvent.clientY - size,
            bottom: mouseEvent.clientY,
            left: mouseEvent.clientX,
            right: mouseEvent.clientX,
            width: 0,
            height: size,
            x: mouseEvent.clientX,
            y: mouseEvent.clientY - size
        };
        isBackward = false;
        isVertical = false;
    }

    if (isInvalidRect(targetRect)) return null;

    return {
        rect: targetRect,
        isBackward: isBackward,
        isVertical: isVertical
    };
}

function initContainer() {
    if (hostElement && hostElement.isConnected) return;

    if (hostElement) {
        hostElement = null;
        shadowRoot = null;
    }

    hostElement = document.createElement('div');
    hostElement.id = 'tm-smart-copy-host';
    hostElement.style.all = 'initial';
    hostElement.style.position = 'fixed';
    hostElement.style.zIndex = '2147483647'; // Max Z-Index
    hostElement.style.top = '0';
    hostElement.style.left = '0';
    hostElement.style.width = '0';
    hostElement.style.height = '0';
    hostElement.style.overflow = 'visible';
    hostElement.style.pointerEvents = 'none';

    (document.documentElement || document.body).appendChild(hostElement);

    shadowRoot = hostElement.attachShadow({ mode: 'open' });

    const style = document.createElement('style');
    style.textContent = getStyles();
    shadowRoot.appendChild(style);
}

function getStyles() {
    const isCol = getConfig('buttonStyle') === 'col';
    const padRow = '10px 13.1415926px';   // 胶囊:上下略小,左右略大
    const padCol = '10px';       // 纵向:正方形,四边一致
    return `
        :host { all: initial; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
        .sc-container {
            position: fixed;
            display: flex;
            flex-direction: ${isCol ? 'column' : 'row'};
            background: rgba(255, 255, 255, 0.15);
            border: 1px solid transparent;
            box-shadow:
                0 0 0 1px rgba(255, 255, 255, 0.3),
                0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.06),
                0 0 10px rgba(255, 255, 255, 0.1);
            color: #000;
            border-radius: ${isCol ? '12px' : '20px'};
            font-size: 16px;
            z-index: 9999;
            cursor: pointer;
            user-select: none;
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            opacity: 0;
            transform: scale(0.95);
            transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
            pointer-events: auto;
            overflow: hidden;
            white-space: nowrap;
        }
        .sc-container.visible {
            opacity: 1;
            transform: scale(1);
        }
        .sc-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s, transform 0.1s;
            color: #000;
            padding: ${isCol ? padCol : padRow};
        }
        .sc-container[data-btn-count="1"] .sc-btn {
            padding: 10px;
            aspect-ratio: 1 / 1;
        }
        .sc-btn:hover {
            background: rgba(255, 255, 255, 0.3);
            transform: scale(1.03);
        }
        .sc-btn:active {
            transform: scale(0.98);
            background: rgba(255, 255, 255, 0.2);
        }
        /* 深色模式覆盖 */
        .theme-dark-ui {
            background: rgba(30, 30, 30, 0.3);
            border: 1px solid transparent;
            box-shadow:
                0 0 0 1px rgba(255, 255, 255, 0.15),
                0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.06),
                0 0 10px rgba(0, 0, 0, 0.1);
            color: #fff;
        }
        .theme-dark-ui .sc-btn {
            color: #fff;
        }
        .theme-dark-ui .sc-btn:hover {
            background: rgba(255, 255, 255, 0.15);
        }
        .theme-dark-ui .sc-btn:active {
            background: rgba(255, 255, 255, 0.1);
        }
        /* 分割线 */
        .divider {
            background: rgba(255, 255, 255, 0.25);
        }
        .theme-dark-ui .divider {
            background: rgba(255, 255, 255, 0.12);
        }
        .divider-v { width: 1px; height: 1.6em; align-self: center; }
        .divider-h { height: 1px; width: 100%; }
        /* Toast 通知 */
        .sc-toast {
            position: fixed;
            left: 50%;
            bottom: 20px;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.6);
            color: white;
            padding: 8px 16px;
            border-radius: 20px;
            font-size: 13px;
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.3s;
            z-index: 10000;
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
        }
        .sc-toast.show { opacity: 1; }
        /* ===== Liquid Glass + HDR Glow ===== */
        .sc-container {
            position: fixed;
            display: flex;
            backdrop-filter: blur(14px) saturate(180%);
            -webkit-backdrop-filter: blur(14px) saturate(180%);
            background:
                linear-gradient(135deg, rgba(255,255,255,0.20), rgba(255,255,255,0.05)),
                url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'>\
<filter id='n'>\
<feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/>\
<feColorMatrix type='saturate' values='0'/>\
<feComponentTransfer><feFuncA type='linear' slope='0.08'/></feComponentTransfer>\
</filter>\
<rect width='40' height='40' filter='url(#n)'/>\
</svg>"),
                rgba(255,255,255,0.10);
            background-blend-mode: overlay;
            box-shadow:
                0 0 0 1px rgba(255,255,255,0.35),
                0 0 12px rgba(255,255,255,0.15),
                0 8px 30px rgba(0,0,0,0.22);
            transition: box-shadow .25s ease, transform .25s ease, opacity .2s ease;
        }
        .sc-btn:hover {
            background: rgba(255,255,255,0.28);
            transform: scale(1.05);
            box-shadow:
                0 0 6px rgba(255,255,255,0.8),
                0 0 16px rgba(255,255,255,0.6),
                0 0 26px rgba(255,255,255,0.4);
            filter: brightness(1.25);
        }
        .theme-dark-ui {
            background:
                linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)),
                url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'>\
<filter id='n'>\
<feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/>\
<feColorMatrix type='saturate' values='0'/>\
<feComponentTransfer><feFuncA type='linear' slope='0.06'/></feComponentTransfer>\
</filter>\
<rect width='40' height='40' filter='url(#n)'/>\
</svg>"),
                rgba(0,0,0,0.25);
            background-blend-mode: soft-light;
            box-shadow:
                0 0 0 1px rgba(255,255,255,0.18),
                0 0 12px rgba(255,255,255,0.06),
                0 8px 26px rgba(0,0,0,0.32);
        }
        .theme-dark-ui .sc-btn:hover {
            background: rgba(255,255,255,0.12);
            filter: brightness(1.35);
            box-shadow:
                0 0 6px rgba(255,255,255,0.5),
                0 0 22px rgba(255,255,255,0.25),
                0 0 36px rgba(255,255,255,0.15);
        }
        /* ===== 玻璃折射边 ===== */
        .theme-light-ui.sc-container {
            background:
                linear-gradient(135deg, rgba(255,255,255,0.25), rgba(255,255,255,0.08)),
                url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'>\
<filter id='n'>\
<feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/>\
<feColorMatrix type='saturate' values='0'/>\
<feComponentTransfer><feFuncA type='linear' slope='0.06'/></feComponentTransfer>\
</filter>\
<rect width='40' height='40' filter='url(#n)'/>\
</svg>"),
                rgba(255,255,255,0.18);
            background-blend-mode: overlay;
            box-shadow:
                inset 2px 2px 3px rgba(0,0,0,0.20),
                inset -2px -2px 3px rgba(0,0,0,0.18),
                0 0 0 1px rgba(255,255,255,0.45),
                0 0 12px rgba(255,255,255,0.25),
                0 8px 30px rgba(0,0,0,0.18);
            --divider-color: rgba(0,0,0,0.18);
        }
        .theme-light-ui .sc-btn:hover {
            background: rgba(255,255,255,0.35);
            filter: brightness(1.3);
            box-shadow:
                0 0 6px rgba(255,255,255,0.9),
                0 0 16px rgba(255,255,255,0.7),
                0 0 26px rgba(255,255,255,0.5);
        }
        .divider {
            background: var(--divider-color, rgba(255,255,255,0.25));
        }
        .theme-dark-ui.sc-container {
            box-shadow:
                inset 2px 2px 3px rgba(255,255,255,0.32),
                inset -2px -2px 3px rgba(255,255,255,0.28),
                0 0 0 1px rgba(255,255,255,0.18),
                0 0 12px rgba(255,255,255,0.06),
                0 8px 26px rgba(0,0,0,0.32);
        }
        /* 图标包装器:作为角标定位锚点,尺寸与SVG一致 */
        .sc-icon-wrap {
            position: relative;
            display: inline-flex;
            width: 18px;
            height: 18px;
        }
        /* 链接数量角标: 右下角对齐图标右下角,叠在图标上层 */
        .sc-badge {
            position: absolute;
            right: 0;
            bottom: 0;
            color: inherit;
            font-size: 10px;
            font-weight: 700;
            line-height: 1;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            pointer-events: none;
            filter:
                drop-shadow(0 0 1px rgba(255,255,255,0.98))
                drop-shadow(0 0 2px rgba(255,255,255,0.9))
                drop-shadow(0 0 3px rgba(255,255,255,0.75))
                drop-shadow(0 0 5px rgba(255,255,255,0.55));
        }
        .sc-badge-key {
            font-size: 0;
            right: -1px;
            bottom: -1px;
        }
        .sc-badge-key svg {
            display: block;
            stroke-width: 4;
        }
        .theme-dark-ui .sc-badge {
            filter:
                drop-shadow(0 0 1px rgba(0,0,0,0.98))
                drop-shadow(0 0 2px rgba(0,0,0,0.9))
                drop-shadow(0 0 3px rgba(0,0,0,0.75))
                drop-shadow(0 0 5px rgba(0,0,0,0.55));
        }
    `;
}

let dragStartData = null; // 临时存储拖拽起点数据
const PREVIEW_WIN_NAME = 'PicKitPreviewWindow';

function handleLinkDragStart(e) {
    if (!getConfig('enableDragPreview')) return;

    const link = e.target.closest('a[href]');

    if (!link || !link.href || link.href.startsWith('javascript:') || link.href.startsWith('#')) {
        dragStartData = null;
        return;
    }

    dragStartData = {
        url: link.href,
        x: e.clientX,
        y: e.clientY,
        timestamp: Date.now()
    };
}

function handleLinkDragEnd(e) {
    if (!dragStartData) return;

    const { x: startX, y: startY, url } = dragStartData;
    const endX = e.clientX;
    const endY = e.clientY;

    /* ---------- 1. 视口外松开直接放弃 ---------- */
    if (
        endX < 0 || endY < 0 ||
        endX > window.innerWidth || endY > window.innerHeight
    ) {
        dragStartData = null;
        return;
    }

    /* ---------- 2. 输入区 / 富文本 / 拖放容器 过滤 ---------- */
    const target = document.elementFromPoint(endX, endY);
    if (target) {
        if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
            dragStartData = null;
            return;
        }
        if (target.closest('[contenteditable="true"]')) {
            dragStartData = null;
            return;
        }
        const dropZone = target.closest('[ondragover],[ondrop]');
        if (dropZone) {
            dragStartData = null;
            return;
        }
    }

    /* ---------- 3. 距离阈值判断 ---------- */
    const dist = Math.hypot(endX - startX, endY - startY);
    if (dist > 30) openPreviewWindow(url); // 距离阈值:30px
    dragStartData = null;
}

async function openPreviewWindow(url) {
    const screen = window.screen;
    const screenW = screen.availWidth;
    const screenH = screen.availHeight;
    const screenLeft = screen.availLeft || 0;
    const screenTop = screen.availTop || 0;

    const GOLDEN_RATIO = 0.618;

    const width = Math.round(screenW * GOLDEN_RATIO);
    const height = Math.round(screenH * GOLDEN_RATIO);

    const left = screenLeft + (screenW - width) / 2;
    const top = screenTop + (screenH - height) / 2;

    const features = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=yes`;
    window.open(url, PREVIEW_WIN_NAME, features);
}

let isUnlockMode = false;
let unlockStyleEl = null;
let startPos = { x: 0, y: 0 };
const modifiedElements = new Set(); //追踪受影响元素的集合

function getUnlockCSS() {
    return `
        /* --- 1. 全局强制可选 (分离 cursor 设置) --- */
        html, body, *:not([data-tm-policy="protected"]), [unselectable] {
            user-select: text !important;
            -webkit-user-select: text !important;
            -moz-user-select: text !important;
            -ms-user-select: text !important;
        }

        /* 修复:html/body 保持 default cursor,防止全局污染 */
        html, body {
            cursor: default !important;
        }

        /* 修复:仅为实际文本元素设置 text cursor */
        p, span, div, h1, h2, h3, h4, h5, h6, li, td, th, pre, code,
        blockquote, article, section, main, aside, header, footer,
        nav, figcaption, label, time, mark, em, strong, i, b, u,
        s, small, cite, dfn, abbr, data, q, sub, sup, kbd, samp,
        var, output, details, summary, address, dl, dt, dd,
        fieldset, legend, caption, tbody, thead, tfoot, tr,
        button:not([disabled]),
        a:not([data-tm-policy="protected"]) {
            cursor: text !important;
        }

        /* 强制高亮颜色 */
        ::selection {background-color: #3390FF !important;color: #ffffff !important;text-shadow: none !important;}
        ::-moz-selection {background-color: #3390FF !important;color: #ffffff !important;text-shadow: none !important;}

        /* 让链接看起来像普通文本,且禁止图片/链接被拖拽(干扰划词) */
        a:not([data-tm-policy="protected"]),
        a *:not([data-tm-policy="protected"]),
        img:not([data-tm-policy="protected"]){
            pointer-events: auto !important;
            user-drag: none !important;
            -webkit-user-drag: none !important;
            text-decoration: none !important;
        }

        /* 禁用常见的透明遮罩层交互,让鼠标穿透到下方文字 */
        div[style*="z-index"][style*="fixed"]:not([data-tm-policy="protected"]),
        div[style*="z-index"][style*="absolute"]:not([data-tm-policy="protected"]) {
            pointer-events: none !important;
        }

        /* 修复:增强 pointer-events 恢复逻辑,覆盖更多容器类型 */
        div:not([data-tm-policy="protected"]),
        article:not([data-tm-policy="protected"]),
        main:not([data-tm-policy="protected"]),
        section:not([data-tm-policy="protected"]),
        aside:not([data-tm-policy="protected"]),
        header:not([data-tm-policy="protected"]),
        footer:not([data-tm-policy="protected"]),
        nav:not([data-tm-policy="protected"]),
        figure:not([data-tm-policy="protected"]),
        figcaption:not([data-tm-policy="protected"]),
        details:not([data-tm-policy="protected"]),
        summary:not([data-tm-policy="protected"]),
        fieldset:not([data-tm-policy="protected"]),
        dialog:not([data-tm-policy="protected"]),
        p:not([data-tm-policy="protected"]),
        span:not([data-tm-policy="protected"]),
        h1:not([data-tm-policy="protected"]), h2:not([data-tm-policy="protected"]),
        h3:not([data-tm-policy="protected"]), h4:not([data-tm-policy="protected"]),
        h5:not([data-tm-policy="protected"]), h6:not([data-tm-policy="protected"]),
        em:not([data-tm-policy="protected"]), strong:not([data-tm-policy="protected"]),
        i:not([data-tm-policy="protected"]), b:not([data-tm-policy="protected"]),
        td:not([data-tm-policy="protected"]), li:not([data-tm-policy="protected"]),
        code:not([data-tm-policy="protected"]), pre:not([data-tm-policy="protected"]) {
            pointer-events: auto !important;
        }

        /* 针对被截断文本展开后的样式:隐藏滚动条但保留滚动功能 */
        .tm-sc-expanded {
            scrollbar-width: none !important;
            -ms-overflow-style: none !important;
        }
        .tm-sc-expanded::-webkit-scrollbar {
            display: none !important;
            width: 0 !important;
            height: 0 !important;
        }

        a.absolute, a[style*="position: absolute"] { pointer-events: none !important; }

        /* 保护标记优先级最高 */
        [data-tm-policy="protected"][data-tm-policy="protected"][data-tm-policy="protected"],
        [data-tm-policy="protected"][data-tm-policy="protected"][data-tm-policy="protected"] * {
            user-select: none !important;
            -webkit-user-select: none !important;
            -moz-user-select: none !important;
            pointer-events: none !important;
            cursor: default !important;
            z-index: 2147483647 !important;
        }
    `;
}

function isProtectedElement(target) {
    return target && target.closest && target.closest('[data-tm-policy="protected"]');
}

function handleCaptureSelectStart(e) {
    if (!isUnlockMode) return;
    if (isProtectedElement(e.target)) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        return;
    }
    e.stopPropagation();
    e.stopImmediatePropagation();
}

function handleCaptureClick(e) {
    if (!isUnlockMode) return;
    if (isProtectedElement(e.target)) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        return;
    }
    const dx = Math.abs(e.clientX - startPos.x);
    const dy = Math.abs(e.clientY - startPos.y);
    const isDrag = dx > 3 || dy > 3;

    let target = e.target;
    let isLink = false;
    while (target && target !== document) {
        if (target.tagName === 'A') {
            isLink = true;
            break;
        }
        target = target.parentNode;
    }
    if (isDrag || isLink) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
    }
}

function handleCaptureMouseDown(e) {
    if (!isUnlockMode) return;
    if (isProtectedElement(e.target)) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        return;
    }
    const el = e.target;
    if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
        try {
            if (el.type === 'password') {
                el.dataset.scOriginalType = 'password';
                el.type = 'text';
                modifiedElements.add(el);
            }
            if (el.disabled) {
                el.disabled = false;
                el.dataset.scWasDisabled = 'true';
                modifiedElements.add(el);
            }
            if (el.readOnly) {
                el.readOnly = false;
                el.dataset.scWasReadOnly = 'true';
                modifiedElements.add(el);
            }
        } catch (err) {}
    }
    startPos = { x: e.clientX, y: e.clientY };
    e.stopPropagation();
    e.stopImmediatePropagation();
}

function handleCaptureDragStart(e) {
    if (!isUnlockMode) return;
    e.preventDefault();
    e.stopPropagation();
}

function handleCaptureCopy(e) {
    if (!isUnlockMode) return;
    e.stopImmediatePropagation();
}

function handleCaptureSelectionChange(e) {
    if (!isUnlockMode) return;
    e.stopPropagation();
    e.stopImmediatePropagation();
}

function cleanInlineEvents() {
    const targets = [document.documentElement, document.body];
    const events = ['onselectstart', 'onmousedown', 'oncontextmenu', 'oncopy'];
    targets.forEach(el => {
        if (!el) return;
        events.forEach(evt => {
            if (el.hasAttribute(evt)) {
                el.removeAttribute(evt);
            }
            if (el[evt]) {
                el[evt] = null;
            }
        });
    });
}

function handleExpandHover(e) {
    if (!isUnlockMode) return;
    let target = e.target;
    if (target.nodeType !== 1 || target.classList.contains('tm-sc-expanded')) return;

    const style = window.getComputedStyle(target);
    const isEllipsis = style.textOverflow === 'ellipsis';
    const isLineClamp = style.webkitLineClamp && style.webkitLineClamp !== 'none';

    if (isEllipsis || isLineClamp) {
        const rect = target.getBoundingClientRect();
        target.style.setProperty('height', rect.height + 'px', 'important');
        target.style.setProperty('width', rect.width + 'px', 'important');
        target.classList.add('tm-sc-expanded');

        if (isLineClamp) {
            target.style.setProperty('-webkit-line-clamp', 'none', 'important');
            target.style.setProperty('overflow-y', 'auto', 'important');
        } else {
            target.style.setProperty('text-overflow', 'clip', 'important');
            target.style.setProperty('overflow-x', 'auto', 'important');
            target.style.setProperty('white-space', 'nowrap', 'important');
        }
    }
}

function cleanupExpandedElements() {
    const elements = document.querySelectorAll('.tm-sc-expanded');
    elements.forEach(el => {
        el.scrollTop = 0;
        el.scrollLeft = 0;
        el.classList.remove('tm-sc-expanded');
        el.style.removeProperty('height');
        el.style.removeProperty('width');
        el.style.removeProperty('-webkit-line-clamp');
        el.style.removeProperty('overflow-y');
        el.style.removeProperty('overflow-x');
        el.style.removeProperty('text-overflow');
        el.style.removeProperty('white-space');
    });
}

function toggleUnlockMode(active) {
    if (active === isUnlockMode) return;
    isUnlockMode = active;

    if (active) {
        if (!unlockStyleEl) {
            unlockStyleEl = document.createElement('style');
            unlockStyleEl.textContent = getUnlockCSS();
            unlockStyleEl.id = 'tm-smart-copy-unlock-style';
        }
        (document.documentElement || document.body).appendChild(unlockStyleEl);
        cleanInlineEvents();

        window.addEventListener('selectstart', handleCaptureSelectStart, true);
        window.addEventListener('click', handleCaptureClick, true);
        window.addEventListener('mousedown', handleCaptureMouseDown, true);
        window.addEventListener('dragstart', handleCaptureDragStart, true);
        window.addEventListener('copy', handleCaptureCopy, true);
        window.addEventListener('contextmenu', handleCaptureCopy, true);
        document.addEventListener('selectionchange', handleCaptureSelectionChange, true);
        document.addEventListener('mouseover', handleExpandHover, true);

        showToast(t('toast_unlock'));
    } else {
        if (unlockStyleEl && unlockStyleEl.parentNode) {
            unlockStyleEl.parentNode.removeChild(unlockStyleEl);
        }

        modifiedElements.forEach(el => {
            try {
                if (el.dataset.scOriginalType === 'password') {
                    el.type = 'password';
                    delete el.dataset.scOriginalType;
                }
                if (el.dataset.scWasDisabled === 'true') { el.disabled = true; delete el.dataset.scWasDisabled; }
                if (el.dataset.scWasReadOnly === 'true') { el.readOnly = true; delete el.dataset.scWasReadOnly; }
            } catch (e) {}
        });
        modifiedElements.clear();

        window.removeEventListener('selectstart', handleCaptureSelectStart, true);
        window.removeEventListener('mousedown', handleCaptureMouseDown, true);
        window.removeEventListener('click', handleCaptureClick, true);
        window.removeEventListener('dragstart', handleCaptureDragStart, true);
        window.removeEventListener('copy', handleCaptureCopy, true);
        window.removeEventListener('contextmenu', handleCaptureCopy, true);
        document.removeEventListener('selectionchange', handleCaptureSelectionChange, true);
        document.removeEventListener('mouseover', handleExpandHover, true);
        cleanupExpandedElements();

        const sel = window.getSelection();
        if (sel && sel.rangeCount > 0) {
            sel.removeAllRanges();
        }
        const toast = shadowRoot && shadowRoot.querySelector('.sc-toast');
        if (toast) toast.classList.remove('show');
    }
}

document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && isEditMode) {
        toggleEditMode(false);
        return;
    }
    const hotkey = getConfig('unlockHotkey');
    if (!hotkey) return;
    if (e.code === hotkey || e.key === hotkey) {
        if (!isUnlockMode) toggleUnlockMode(true);
    }
});

document.addEventListener('keyup', (e) => {
    const hotkey = getConfig('unlockHotkey');
    if (!hotkey) return;
    if (e.code === hotkey || e.key === hotkey) {
        if (isUnlockMode) toggleUnlockMode(false);
    }
});

window.addEventListener('blur', () => {
    if (isUnlockMode) toggleUnlockMode(false);
});

async function copyToClipboard(text, html) {
    try {
        if (html && typeof ClipboardItem !== 'undefined') {
            const htmlBlob = new Blob([html], { type: 'text/html' });
            const textBlob = new Blob([text], { type: 'text/plain' });
            const data = [new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob })];
            await navigator.clipboard.write(data);
        } else {
            await navigator.clipboard.writeText(text);
        }
    } catch (e) {
        if (typeof GM_setClipboard === 'function') {
            if (text) {
                GM_setClipboard(text, 'text');
            } else {
                GM_setClipboard(html, 'html');
            }
        }
    }
}

function showToast(msg) {
    if (!getConfig('enableToast')) return;

    let toast = shadowRoot.querySelector('.sc-toast');
    if (!toast) {
        toast = document.createElement('div');
        toast.className = 'sc-toast';
        shadowRoot.appendChild(toast);
    }
    toast.textContent = msg;
    toast.classList.add('show');

    if (toastTimer) clearTimeout(toastTimer);
    toastTimer = setTimeout(() => {
        toast.classList.remove('show');
    }, 1200);
}

function getBestContrastTheme() {
    const getBgColor = (el) => {
        if (!el) return null;
        const style = window.getComputedStyle(el);
        return style.backgroundColor;
    };

    const getBrightness = (colorStr) => {
        if (!colorStr || colorStr === 'transparent' || colorStr === 'rgba(0, 0, 0, 0)') return null;
        const match = colorStr.match(/(\d+),\s*(\d+),\s*(\d+)/);
        if (!match) return null;
        const [r, g, b] = [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
        return (r * 299 + g * 587 + b * 114) / 1000;
    };

    let brightness = getBrightness(getBgColor(document.body));

    if (brightness === null) {
        brightness = getBrightness(getBgColor(document.documentElement));
    }

    if (brightness === null) {
        const sysIsDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        return sysIsDark ? 'theme-light-ui' : 'theme-dark-ui';
    }

    return brightness < 128 ? 'theme-light-ui' : 'theme-dark-ui';
}

function renderButton(rect, mouseX, mouseY, text, html, mode = 'default', targetInput = null, isEditable = false, pasteCache = null) {
    const oldBtn = shadowRoot.querySelector('.sc-container');
    if (oldBtn) oldBtn.remove();

    const container = document.createElement('div');
    container.className = 'sc-container';

    const forceWB = getConfig('forceWhiteBlack');
    if (forceWB) {
        container.classList.add('theme-light-ui');
    } else {
        const contrastTheme = getBestContrastTheme();
        container.classList.add(contrastTheme);
    }

    const isCol = getConfig('buttonStyle') === 'col';

    if (isEditMode) {
        const delBtn = document.createElement('div');
        delBtn.className = 'sc-btn';
        delBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`;
        delBtn.title = t('btn_delete');
        delBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
        delBtn.onclick = (e) => {
            e.stopPropagation();
            document.execCommand('delete');
            hideUI();
        };
        container.appendChild(delBtn);

        const div1 = document.createElement('div');
        div1.className = isCol ? 'divider divider-h' : 'divider divider-v';
        container.appendChild(div1);

        const boldBtn = document.createElement('div');
        boldBtn.className = 'sc-btn';
        boldBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path></svg>`;
        boldBtn.title = t('btn_bold');
        boldBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
        boldBtn.onclick = (e) => {
            e.stopPropagation();
            document.execCommand('bold');
        };
        container.appendChild(boldBtn);

        const div2 = document.createElement('div');
        div2.className = isCol ? 'divider divider-h' : 'divider divider-v';
        container.appendChild(div2);

        const highlightBtn = document.createElement('div');
        highlightBtn.className = 'sc-btn';
        highlightBtn.innerHTML = `<?xml version="1.0" encoding="UTF-8"?><svg width="18" height="18" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 44L6 25H12V17H36V25H42V44H6Z" fill="none" stroke="#000000" stroke-width="4" stroke-linejoin="bevel"/><path d="M17 17V8L31 4V17" stroke="#000000" stroke-width="4" stroke-linecap="round" stroke-linejoin="bevel"/></svg>`;
        highlightBtn.title = t('btn_highlight');
        highlightBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
        highlightBtn.onclick = (e) => {
            e.stopPropagation();
            if (!document.execCommand('hiliteColor', false, 'yellow')) {
                document.execCommand('backColor', false, 'yellow');
            }
            hideUI();
        };
        container.appendChild(highlightBtn);
    }
    else if (mode === 'default' || mode === PASTE_MODE_THREE_BTNS) {
        const copyBtn = document.createElement('div');
        copyBtn.className = 'sc-btn';
        copyBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
        copyBtn.title = t('btn_copy');
        copyBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
        copyBtn.onclick = async (e) => {
            e.stopPropagation();
            triggerSpringFestivalEffect(e.clientX, e.clientY, shadowRoot);
            const contentToCopy = getConfig('enableCache') ? (cachedSelection.text || text) : text;
            const htmlToCopy = getConfig('enableCache') ? (cachedSelection.html || html) : html;
            await copyToClipboard(contentToCopy, htmlToCopy);
            if (getConfig('enablePaste')) {
                await safeSetValue('smart_paste_cache', {
                    text: contentToCopy,
                    timestamp: Date.now()
                });
                unregisterVisibilityCleanup();
            }
            showToast(getSpringFestivalToastText());
            setTimeout(hideUI, 50);
        };
        container.appendChild(copyBtn);

        const isInInput = targetInput !== null;

        if (isInInput && !isEditMode) {
            const div = document.createElement('div');
            div.className = isCol ? 'divider divider-h' : 'divider divider-v';
            container.appendChild(div);

            const cutBtn = document.createElement('div');
            cutBtn.className = 'sc-btn';
            cutBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><line x1="20" y1="4" x2="8.12" y2="15.88"></line><line x1="14.47" y1="14.48" x2="20" y2="20"></line><line x1="8.12" y1="8.12" x2="12" y2="12"></line></svg>`;
            cutBtn.title = t('btn_cut');
            cutBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
            cutBtn.onclick = async (e) => {
                e.stopPropagation();
                triggerSpringFestivalEffect(e.clientX, e.clientY, shadowRoot);
                const contentToCopy = getConfig('enableCache') ? (cachedSelection.text || text) : text;
                const htmlToCopy = getConfig('enableCache') ? (cachedSelection.html || html) : html;
                try {
                    const success = document.execCommand('cut');
                    if (!success) throw new Error('execCommand failed');
                } catch (err) {
                    await copyToClipboard(contentToCopy, htmlToCopy);
                    const selection = window.getSelection();
                    if (selection.rangeCount > 0) {
                        selection.getRangeAt(0).deleteContents();
                    }
                }
                if (getConfig('enablePaste')) {
                    await safeSetValue('smart_paste_cache', {
                        text: contentToCopy,
                        timestamp: Date.now()
                    });
                    unregisterVisibilityCleanup();
                }
                setTimeout(hideUI, 35);
            };
            container.appendChild(cutBtn);
        }

        if (getConfig('enableDeleteBtn') && isInInput) {
            const div2 = document.createElement('div');
            div2.className = isCol ? 'divider divider-h' : 'divider divider-v';
            container.appendChild(div2);

            const delBtn = document.createElement('div');
            delBtn.className = 'sc-btn';
            delBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`;
            delBtn.title = t('btn_delete');
            delBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
            delBtn.onclick = (e) => {
                e.stopPropagation();
                document.execCommand('delete');
                hideUI();
            };
            container.appendChild(delBtn);
        }
        else if (!isInInput && !isEditMode && text.trim().length <= 32) {
            const div = document.createElement('div');
            div.className = isCol ? 'divider divider-h' : 'divider divider-v';
            container.appendChild(div);

            const searchBtn = document.createElement('div');
            searchBtn.className = 'sc-btn';
            searchBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`;
            searchBtn.title = t('btn_search');
            searchBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
            searchBtn.onclick = (e) => {
                e.stopPropagation();
                const query = getConfig('enableCache') ? (cachedSelection.text || text) : text;
                const rawText = getConfig('enableCache') ? (cachedSelection.text || text) : text;
                let engine;
                if (getConfig('smartEngine') && !/[\u4e00-\u9fa5]/.test(rawText)) {
                    engine = getConfig('fallbackEngine');
                } else {
                    engine = getConfig('searchEngine');
                }
                let url = SEARCH_ENGINES[engine] ? SEARCH_ENGINES[engine].url : (engine.includes('%s') ? engine : SEARCH_ENGINES['google'].url);
                safeOpenTab(url.replace('%s', encodeURIComponent(query.trim())), { active: true });
                setTimeout(hideUI, 50);
            };
            container.appendChild(searchBtn);
        }

        // @按钮 / 锁链按钮逻辑
        const activeEl = document.activeElement;
        const isUserEditing = activeEl && (
            (['INPUT', 'TEXTAREA'].includes(activeEl.tagName) && !activeEl.readOnly) ||
            activeEl.isContentEditable ||
            document.designMode === 'on'
        );
        if (!isUserEditing && !targetInput && mode !== PASTE_MODE_THREE_BTNS) {
            const emailAddr = extractEmailFromText(text);
            if (emailAddr) {
                const div = document.createElement('div');
                div.className = isCol ? 'divider divider-h' : 'divider divider-v';
                container.appendChild(div);

                const atBtn = document.createElement('div');
                atBtn.className = 'sc-btn';
                atBtn.innerHTML = `<svg viewBox="0 0 48 48" width="18" height="18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44V44C28.9886 44 33.5507 42.1735 37.0539 39.1529" stroke="currentColor" stroke-width="3" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M24 32C28.4183 32 32 28.4183 32 24C32 19.5817 28.4183 16 24 16C19.5817 16 16 19.5817 16 24C16 28.4183 19.5817 32 24 32Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="miter"/><path d="M32 24C32 27.3137 34.6863 30 38 30V30C41.3137 30 44 27.3137 44 24" stroke="currentColor" stroke-width="3" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M32 25V16" stroke="currentColor" stroke-width="3" stroke-linecap="butt" stroke-linejoin="miter"/></svg>`;
                atBtn.title = t('btn_email');
                atBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
                atBtn.onclick = async (e) => {
                    e.stopPropagation();
                    try {
                        await navigator.clipboard.writeText(emailAddr);
                    } catch (_) {
                        if (typeof GM_setClipboard === 'function') {
                            GM_setClipboard(emailAddr, 'text');
                        }
                    }
                    showToast(t('toast_email_copied'));
                    hideUI();
                };
                container.appendChild(atBtn);
            } else {
                const curLangForLink = getConfig('language');
                const isChineseForLink = curLangForLink === 'zh-CN' || (curLangForLink === 'auto' && navigator.language.startsWith('zh'));
                const linkData = isChineseForLink ? extractLinkAndCode(text) : (() => {
                    const urls = extractUrlsFromText(text);
                    return urls.length > 0 ? { urls, password: null } : null;
                })();
                if (linkData && linkData.urls && linkData.urls.length > 0) {
                    const div = document.createElement('div');
                    div.className = isCol ? 'divider divider-h' : 'divider divider-v';
                    container.appendChild(div);

                    const urlCount = linkData.urls.length;
                    const chainBtn = document.createElement('div');
                    chainBtn.className = 'sc-btn';

                    const iconWrap = document.createElement('span');
                    iconWrap.className = 'sc-icon-wrap';
                    iconWrap.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`;
                    chainBtn.appendChild(iconWrap);

                    const isSingleLink = urlCount === 1;
                    if (isChineseForLink && isSingleLink && linkData.password) {
                        const badge = document.createElement('span');
                        badge.className = 'sc-badge sc-badge-key';
                        badge.innerHTML = `<svg viewBox="0 0 48 48" width="10" height="10" stroke="currentColor" fill="none"><path d="M22.8682 24.2982C25.4105 26.7935 26.4138 30.4526 25.4971 33.8863C24.5805 37.32 21.8844 40.0019 18.4325 40.9137C14.9806 41.8256 11.3022 40.8276 8.79375 38.2986C5.02208 34.4141 5.07602 28.2394 8.91499 24.4206C12.754 20.6019 18.9613 20.5482 22.8664 24.3L22.8682 24.2982Z"/><path d="M23 24L40 7"/><path d="M30.3052 16.9001L35.7337 22.3001L42.0671 16.0001L36.6385 10.6001L30.3052 16.9001Z"/></svg>`;
                        iconWrap.appendChild(badge);
                    } else if (urlCount > 1) {
                        const badge = document.createElement('span');
                        badge.className = 'sc-badge';
                        badge.textContent = urlCount;
                        iconWrap.appendChild(badge);
                    }

                    const titlePrefix = urlCount > 1 ? ('(' + urlCount + ') ') : '';
                    chainBtn.title = titlePrefix + t('btn_open_link');
                    chainBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
                    chainBtn.onclick = async (e) => {
                        e.stopPropagation();
                        if (isSingleLink && isChineseForLink && getConfig('enablePaste') && linkData.password) {
                            await safeSetValue('smart_paste_cache', {
                                text: linkData.password,
                                timestamp: Date.now() + 22000,
                                type: 'pan_code'
                            });
                            unregisterVisibilityCleanup();
                            if (getConfig('enableToast')) {
                                showToast(t('toast_password_pasted'));
                            }
                        }
                        linkData.urls.forEach((u, i) => {
                            setTimeout(() => {
                                safeOpenTab(u.url, { active: i === 0 });
                            }, i * 200);
                        });
                        hideUI();
                    };
                    container.appendChild(chainBtn);
                }
            }
        }

        const curLang = getConfig('language');
        const isChineseEnv = curLang === 'zh-CN' || (curLang === 'auto' && navigator.language.startsWith('zh'));
        if (isChineseEnv && targetInput) {
            const isInputType = targetInput.tagName === 'INPUT';
            if (smartCorrectText(text, isInputType) !== null) {
                const div = document.createElement('div');
                div.className = isCol ? 'divider divider-h' : 'divider divider-v';
                container.appendChild(div);

                const correctBtn = document.createElement('div');
                correctBtn.className = 'sc-btn';
                correctBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><path d="M9 15l2 2 4-4"></path></svg>`;
                correctBtn.title = "校正";
                correctBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
                correctBtn.onclick = (e) => {
                    e.stopPropagation();
                    handleTextCorrection(targetInput, text);
                };
                container.appendChild(correctBtn);
            }
        }

        if (mode === PASTE_MODE_THREE_BTNS) {
            const div = document.createElement('div');
            div.className = isCol ? 'divider divider-h' : 'divider divider-v';
            container.appendChild(div);
            const pasteBtn = document.createElement('div');
            pasteBtn.className = 'sc-btn';
            pasteBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>`;
            pasteBtn.title = t('btn_paste');
            pasteBtn.onmousedown = e => { e.preventDefault(); e.stopPropagation(); };
            pasteBtn.onclick = async (e) => {
                e.stopPropagation();
                const cache = await safeGetValue('smart_paste_cache', null);
                if (cache && cache.text) {
                    performPaste(document.activeElement, cache.text);
                    await safeSetValue('smart_paste_cache', {
                        text: cache.text,
                        timestamp: Date.now()
                    });
                    registerVisibilityCleanup();
                }
                hideUI();
            };
            container.appendChild(pasteBtn);
        }
    }
    else if (mode === 'paste') {
        const isPanCode = pasteCache && pasteCache.type === 'pan_code';
        const pasteBtn = document.createElement('div');
        pasteBtn.className = 'sc-btn';
        if (isPanCode) {
            pasteBtn.innerHTML = '<svg viewBox="0 0 48 48" width="18" height="18" stroke="currentColor" stroke-width="3" fill="none"><path d="M22.8682 24.2982C25.4105 26.7935 26.4138 30.4526 25.4971 33.8863C24.5805 37.32 21.8844 40.0019 18.4325 40.9137C14.9806 41.8256 11.3022 40.8276 8.79375 38.2986C5.02208 34.4141 5.07602 28.2394 8.91499 24.4206C12.754 20.6019 18.9613 20.5482 22.8664 24.3L22.8682 24.2982Z"/><path d="M23 24L40 7"/><path d="M30.3052 16.9001L35.7337 22.3001L42.0671 16.0001L36.6385 10.6001L30.3052 16.9001Z"/></svg>';
            pasteBtn.title = t('btn_paste') + ' ' + (pasteCache.text || '');
        } else {
            pasteBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
            pasteBtn.title = t('btn_paste');
        }
        pasteBtn.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); };
        pasteBtn.onclick = async (e) => {
            e.stopPropagation();
            if (isPanCode) {
                performPaste(targetInput || document.activeElement, pasteCache.text);
                showToast(t('toast_password_pasted'));
                await safeSetValue('smart_paste_cache', { text: '', timestamp: 0 });
                unregisterVisibilityCleanup();
                hideUI();
                return;
            }
            performPaste(targetInput, text);
            const existingCache = await safeGetValue('smart_paste_cache', null);
            if (existingCache && existingCache.text) {
                await safeSetValue('smart_paste_cache', {
                    text: existingCache.text,
                    timestamp: Date.now()
                });
                registerVisibilityCleanup();
            }
            hideUI();
        };
        container.appendChild(pasteBtn);
    }

    const btnCount = container.children.length;
    container.setAttribute('data-btn-count', btnCount);

    shadowRoot.appendChild(container);

    container.style.left = '-9999px';

    requestAnimationFrame(() => {
        const btnRect = container.getBoundingClientRect();
        const btnW = btnRect.width;
        const btnH = btnRect.height;
        const offset = getConfig('offset');
        const viewportW = window.innerWidth;
        const viewportH = window.innerHeight;

        let targetX, targetY;

        if (rect) {
            const isBackward = rect.isBackward || false;
            const isVertical = rect.isVertical || false;
            if (isVertical) {
                if (isBackward) {
                    targetX = rect.right + offset;
                    targetY = rect.top;
                } else {
                    targetX = rect.left - btnW - offset;
                    targetY = rect.bottom - btnH;
                }
            } else {
                if (isBackward) {
                    targetX = rect.left - (btnW / 2);
                    targetY = rect.top - btnH - offset;
                } else {
                    targetX = rect.right - (btnW / 2);
                    const spaceBelow = viewportH - rect.bottom;
                    if (spaceBelow < (btnH + offset + 20)) {
                        targetY = rect.top - btnH - offset;
                    } else {
                        targetY = rect.bottom + offset;
                    }
                }
            }
        } else {
            if (mouseY > viewportH / 2) {
                targetY = mouseY - btnH - offset;
            } else {
                targetY = mouseY + offset;
            }
            if (mouseX > viewportW / 2) {
                targetX = mouseX - btnW - offset;
            } else {
                targetX = mouseX + offset;
            }
        }

        const margin = 10;
        targetX = Math.max(margin, Math.min(targetX, viewportW - btnW - margin));
        targetY = Math.max(margin, Math.min(targetY, viewportH - btnH - margin));

        container.style.left = `${targetX}px`;
        container.style.top = `${targetY}px`;
        container.classList.add('visible');

        const timeout = getConfig('timeout');
        if (timeout > 0) {
            if (uiTimer) clearTimeout(uiTimer);
            uiTimer = setTimeout(hideUI, timeout);
        }
    });
}

function hideUI() {
    const btn = shadowRoot && shadowRoot.querySelector('.sc-container');
    if (btn) {
        btn.classList.remove('visible');
        setTimeout(() => {
            if (btn && btn.parentNode) btn.remove();
        }, 200);
    }
    cachedSelection = { text: '', html: '' };
}

function handleSelectionMouseUp(e) {
    if (hostElement && e.composedPath().includes(hostElement)) return;
    if (!hostElement) initContainer();
    if (isScrolling) return;
    setTimeout(async () => {
        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0) {
            hideUI();
            return;
        }
        const text = selection.toString();
        if (!text || text.trim().length === 0) {
            hideUI();
            return;
        }
        const range = selection.getRangeAt(0);
        if (getConfig('enableCache')) {
            const container = document.createElement('div');
            container.appendChild(range.cloneContents());
            cachedSelection = {
                text: text,
                html: container.innerHTML
            };
        }
        let rect = null;
        if (getConfig('positionMode') === 'endchar') {
            const smartState = getSmartSelectionState(selection, e);
            if (smartState) {
                rect = smartState.rect;
                if (rect) {
                    rect.isBackward = smartState.isBackward;
                    rect.isVertical = smartState.isVertical;
                }
            }
        }

        initContainer();
        let cache = null;
        if (getConfig('enablePaste')) {
            cache = await safeGetValue('smart_paste_cache', null);
        }
        const curLang = getConfig('language');
        const isChineseEnv = curLang === 'zh-CN' || (curLang === 'auto' && navigator.language.startsWith('zh'));
        const cacheValid = cache && cache.text && (Date.now() - cache.timestamp < 8000) && !(isChineseEnv && cache.type === 'pan_code');
        const target = document.activeElement;
        const isInput = target && (
            (['INPUT', 'TEXTAREA'].includes(target.tagName) && !target.disabled && !target.readOnly) ||
            target.isContentEditable
        );
        const mode = (cacheValid && isInput) ? PASTE_MODE_THREE_BTNS : 'default';
        renderButton(rect, e.clientX, e.clientY, text, cachedSelection.html || '', mode, isInput ? target : null, isInput, cache);
    }, 10);
}

function handleGlobalMouseDown(e) {
    if (hostElement && e.composedPath().includes(hostElement)) {
    } else {
        const btn = shadowRoot && shadowRoot.querySelector('.sc-container');
        if (btn) btn.classList.remove('visible');
    }
}

const handleResizeOrScroll = () => {
    if (!hostElement) return;
    const mode = getConfig('scrollRepaintMode');
    const btn = shadowRoot.querySelector('.sc-container');
    if (!btn) return;

    if (mode === SCROLL_REPAINT_MODE.HIDE) {
        hideUI();
        return;
    }

    if (mode === SCROLL_REPAINT_MODE.VIEWPORT) {
        const selection = window.getSelection();
        if (!selection.rangeCount) { hideUI(); return; }
        const rect = selection.getRangeAt(0).getBoundingClientRect();
        const inViewport = rect.top >= 0 && rect.left >= 0 &&
            rect.bottom <= window.innerHeight &&
            rect.right <= window.innerWidth;
        if (!inViewport) { hideUI(); return; }
    }

    btn.classList.remove('visible');
    isScrolling = true;
    if (scrollTimeout) clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(() => {
        isScrolling = false;
        const selection = window.getSelection();
        if (selection && selection.toString().trim().length > 0) {
            const range = selection.getRangeAt(0);
            const rects = range.getClientRects();
            if (rects.length > 0) {
                const rect = rects[rects.length - 1];
                renderButton(rect, rect.right, rect.top,
                    selection.toString(),
                    getConfig('enableCache') ? cachedSelection.html : '');
            }
        }
    }, 300);
};

function handleContextMenu(e) {
    hideUI();
    if (getConfig('enablePaste')) {
        safeSetValue('smart_paste_cache', { text: '', timestamp: 0 });
        unregisterVisibilityCleanup();
    }
}

function handleKeydownHideUI(e) {
    if (isUnlockMode) return;
    hideUI();
}

function handleInputPasteMouseUp(e) {
    if (!getConfig('enablePaste')) return;
    const target = e.target;
    const isInput = (['INPUT', 'TEXTAREA'].includes(target.tagName) && !target.disabled && !target.readOnly) || target.isContentEditable;
    if (!isInput) return;
    setTimeout(async () => {
        const cache = await safeGetValue('smart_paste_cache', null);
        if (!cache || !cache.text) return;
        if (Date.now() - cache.timestamp > 8000) return;

        const curLang = getConfig('language');
        const isChineseEnv = curLang === 'zh-CN' || (curLang === 'auto' && navigator.language.startsWith('zh'));
        if (isChineseEnv && cache.type === 'pan_code') {
            initContainer();
            const rect = target.getBoundingClientRect();
            renderButton(rect, e.clientX, e.clientY, cache.text, '', 'paste', target, false, cache);
            return;
        }

        let selectedText = '';
        let hasSelection = false;
        if (['INPUT', 'TEXTAREA'].includes(target.tagName)) {
            const start = target.selectionStart;
            const end = target.selectionEnd;
            if (typeof start === 'number' && typeof end === 'number' && start !== end) {
                selectedText = target.value.substring(start, end);
                hasSelection = true;
            }
        } else if (target.isContentEditable) {
            const sel = window.getSelection();
            if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
                selectedText = sel.toString();
                hasSelection = true;
            }
        }

        const isReplaceIntent = selectedText === ' ' || selectedText === ',';
        const mode = (hasSelection && !isReplaceIntent) ? PASTE_MODE_THREE_BTNS : 'paste';
        const textArg = (mode === 'paste') ? cache.text : selectedText;

        let rect = null;
        if (target.isContentEditable && hasSelection) {
            const sel = window.getSelection();
            if (sel.rangeCount > 0) {
                const range = sel.getRangeAt(0);
                const rects = range.getClientRects();
                if (rects.length > 0) {
                    rect = rects[rects.length - 1];
                }
            }
        }
        if (!hostElement) initContainer();
        renderButton(rect, e.clientX, e.clientY, textArg, '', mode, target, false, cache);
    }, 20);
}

function smartCorrectText(text, isInputType) {
    const hasHanzi = /[\u4e00-\u9fa5]/.test(text);
    const hasCNPunct = /[,。:;?!""''()【】《》]/.test(text);
    const hasNum = /\d/.test(text);

    let activeRules = {
        basic: hasHanzi,
        punct: hasHanzi || hasCNPunct,
        unit: hasHanzi || hasCNPunct || hasNum,
        pureCN: hasHanzi && !/[a-zA-Z]/.test(text.replace(/[a-zA-Z]+(?=[%℃$])/, ''))
    };

    if (!activeRules.basic && !activeRules.punct && !activeRules.unit) return null;

    const applyRule = (txt, regex, replacement) => {
        const parts = txt.split(/(".*?"|".*?")/g);
        return parts.map((part, i) => {
            if (i % 2 === 1) return part;
            return part.replace(regex, replacement);
        }).join('');
    };

    let result = text;

    if (activeRules.basic) {
        const rule9Regex = /([\u4e00-\u9fa5。])(\s{2,})(?=[\u4e00-\u9fa5]|\d{1,3}(?:[、.]|\s))/g;
        result = applyRule(result, rule9Regex, (match, p1, p2) => {
            return p1 + (isInputType ? '' : '\n');
        });
    }

    if (activeRules.pureCN) {
        const parts = result.split(/(".*?"|".*?")/g);
        result = parts.map((part, i) => {
            if (i % 2 === 1) return part;
            let p = part;
            p = p.replace(/\.{3,}/g, '……');
            p = p.replace(/\.{2}/g, '。');
            p = p.replace(/(?<!\d)\.(?!\d)|(?<=\d)\.(?!\d)|(?<!\d)\.(?=\d)/g, '。');
            const map = {',':',', '?':'?', '!':'!', ':':':', ';':';', '(':'(', ')':')'};
            p = p.replace(/[,?!:;()]/g, m => map[m]);
            return p;
        }).join('');
    }

    if (activeRules.basic) {
        result = applyRule(result, /([\u4e00-\u9fa5])([a-zA-Z])/g, '$1 $2');
        result = applyRule(result, /([a-zA-Z])([\u4e00-\u9fa5])/g, '$1 $2');
    }

    if (activeRules.basic) {
        const isMathContext = /[+*/=]|等于/.test(text);
        const charSet = isMathContext ? '[\\d+\\-*/=]' : '[\\d]';
        const regex1 = new RegExp(`([\\u4e00-\\u9fa5])(?=${charSet})`, 'g');
        const regex2 = new RegExp(`(${charSet})(?=[\\u4e00-\\u9fa5])`, 'g');
        result = applyRule(result, regex1, '$1 ');
        result = applyRule(result, regex2, '$1 ');
    }

    if (activeRules.punct) {
        result = applyRule(result, /([a-zA-Z0-9\u4e00-\u9fa5])\s+([,.:;?!,。:;?!、\])}()】【《》[({""''"'])/g, '$1$2');
    }

    if (activeRules.unit) {
        result = applyRule(result, /(\d)\s+([%℃$])/g, '$1$2');
        result = applyRule(result, /([^\s\d])([%℃$])/g, '$1 $2');
    }

    if (activeRules.basic) {
        const parts = result.split(/(".*?"|".*?")/g);
        result = parts.map((part, i) => {
            if (i % 2 === 1) return part;
            part = part.replace(/。{3,8}/g, '……');
            part = part.replace(/。{2}/g, '。');
            return part;
        }).join('');
    }

    if (activeRules.unit) {
        result = applyRule(result, /(\d)\s*:\s*(\d)/g, '$1:$2');
    }

    if (activeRules.punct) {
        const quoteCount = (result.match(/[""]/g) || []).length;
        if (quoteCount === 2) {
            let qIndex = 0;
            result = result.replace(/[""]/g, () => {
                qIndex++;
                return qIndex === 1 ? '\u201C' : '\u201D';
            });
        }
    }

    return result === text ? null : result;
}

async function handleTextCorrection(target, originalText) {
    const isInput = target.tagName === 'INPUT';
    const newText = smartCorrectText(originalText, isInput);

    if (!newText) {
        showToast('无需校正');
        return;
    }

    if (document.execCommand && typeof document.execCommand === 'function') {
        try {
            target.focus();
            document.execCommand('insertText', false, newText);
        } catch (e) {
            performPaste(target, newText);
        }
    } else {
        performPaste(target, newText);
    }

    showToast('文本已校正');
    hideUI();
}

function performPaste(target, text) {
    if (!target) return;
    target.focus();

    try {
        const success = document.execCommand('insertText', false, text);
        if (success) {
            showToast(t('toast_pasted'));
            return;
        }
    } catch (e) {}

    try {
        if (target.isContentEditable) {
            const sel = window.getSelection();
            if (sel.rangeCount > 0) {
                const range = sel.getRangeAt(0);
                range.deleteContents();
                range.insertNode(document.createTextNode(text));
                range.collapse(false);
            } else {
                target.innerText += text;
            }
        } else {
            const start = target.selectionStart || 0;
            const end = target.selectionEnd || 0;
            const oldVal = target.value;
            const newVal = oldVal.slice(0, start) + text + oldVal.slice(end);

            let proto = window.HTMLInputElement.prototype;
            if (target.tagName === 'TEXTAREA') {
                proto = window.HTMLTextAreaElement.prototype;
            }

            const nativeValueSetter = Object.getOwnPropertyDescriptor(proto, "value").set;
            if (nativeValueSetter && nativeValueSetter.call) {
                nativeValueSetter.call(target, newVal);
            } else {
                target.value = newVal;
            }

            target.dispatchEvent(new Event('input', { bubbles: true }));
            target.dispatchEvent(new Event('change', { bubbles: true }));

            const newCursorPos = start + text.length;
            target.setSelectionRange(newCursorPos, newCursorPos);
        }
        showToast(t('toast_paste_compat'));
    } catch (e) {
        showToast(t('toast_paste_fail'));
    }
}

let pickerOverlay = null;
let pickerHandler = null;
let pickerClickHandler = null;
let pickerEscHandler = null;
let pickerRightClickHandler = null;

function applySavedBlockingRules() {
    const rules = configCache['blocked_elements'] || {};
    const domain = location.hostname;
    if (rules[domain] && Array.isArray(rules[domain])) {
        const cssText = rules[domain].join(', ') + ' { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; }';
        GM_addStyle(cssText);
    }
}

function activateElementPicker() {
    if (pickerOverlay) disablePicker();

    showToast(t('picker_active'));

    pickerOverlay = document.createElement('div');
    pickerOverlay.style.all = 'initial';
    pickerOverlay.style.position = 'fixed';
    pickerOverlay.style.pointerEvents = 'none';
    pickerOverlay.style.border = '2px solid #ff0000';
    pickerOverlay.style.background = 'rgba(255, 0, 0, 0.1)';
    pickerOverlay.style.zIndex = '2147483646';
    pickerOverlay.style.transition = 'all 0.1s ease';
    pickerOverlay.style.display = 'none';
    document.body.appendChild(pickerOverlay);

    pickerHandler = (e) => {
        const target = e.target;
        if (target === hostElement || hostElement.contains(target) || target === pickerOverlay) return;
        const rect = target.getBoundingClientRect();
        pickerOverlay.style.display = 'block';
        pickerOverlay.style.top = rect.top + 'px';
        pickerOverlay.style.left = rect.left + 'px';
        pickerOverlay.style.width = rect.width + 'px';
        pickerOverlay.style.height = rect.height + 'px';
    };

    pickerClickHandler = (e) => {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();

        const target = e.target;
        if (target === hostElement || hostElement.contains(target)) {
            showToast(t('picker_cant_block_self'));
            return;
        }

        const selector = generateCssSelector(target);
        if (confirm(t('picker_confirm', selector) + `\n(Domain: ${location.hostname})`)) {
            saveBlockRule(selector);
            target.style.display = 'none';
            showToast(t('picker_saved'));
            disablePicker();
        }
    };

    pickerEscHandler = (e) => {
        if (e.key === 'Escape') {
            disablePicker();
            showToast(t('picker_exit'));
        }
    };

    pickerRightClickHandler = (e) => {
        e.preventDefault();
        e.stopPropagation();
        disablePicker();
    };

    document.addEventListener('contextmenu', pickerRightClickHandler, true);
    document.addEventListener('mousemove', pickerHandler, true);
    document.addEventListener('click', pickerClickHandler, true);
    document.addEventListener('keydown', pickerEscHandler, true);
}

function disablePicker() {
    if (pickerOverlay) {
        pickerOverlay.remove();
        pickerOverlay = null;
    }
    document.removeEventListener('mousemove', pickerHandler, true);
    document.removeEventListener('click', pickerClickHandler, true);
    document.removeEventListener('keydown', pickerEscHandler, true);
    document.removeEventListener('contextmenu', pickerRightClickHandler, true);
    pickerRightClickHandler = null;
}

function generateCssSelector(el) {
    if (el.id) return '#' + CSS.escape(el.id);

    const tagName = el.tagName.toLowerCase();
    let selector = tagName;

    if (el.className && typeof el.className === 'string' && el.className.trim().length > 0) {
        const classes = el.className.trim().split(/\s+/);
        classes.slice(0, 3).forEach(c => {
            selector += '.' + CSS.escape(c);
        });
    }

    if (selector === tagName) {
        if (el.parentElement && el.parentElement !== document.body) {
            return generateCssSelector(el.parentElement) + ' > ' + tagName;
        }
    }

    return selector;
}

function saveBlockRule(selector) {
    const rules = configCache['blocked_elements'] || {};
    const domain = location.hostname;

    if (!rules[domain]) rules[domain] = [];
    if (!rules[domain].includes(selector)) {
        rules[domain].push(selector);
        configCache['blocked_elements'] = rules;
        safeSetValue('blocked_elements', rules);
    }
}

function getFestivalType() {
    const now = new Date();
    try {
        const formatter = new Intl.DateTimeFormat("zh-CN-u-ca-chinese", { month: "numeric", day: "numeric" });
        if (formatter.resolvedOptions().calendar === 'chinese') {
            const parts = formatter.formatToParts(now);
            const monthPart = parts.find(p => p.type === 'month').value;
            const dayPart = parts.find(p => p.type === 'day').value;

            const isLunarJan = monthPart.includes('正') || monthPart.replace(/[^\d]/g, '') === '1';
            const day = parseInt(dayPart.replace(/[^\d]/g, ''));

            if (isLunarJan && day === 1) return 'CNY';
            return 'NONE';
        }
    } catch (e) {}

    if (now.getMonth() === 11 && now.getDate() === 25) {
        return 'XMAS';
    }

    return 'NONE';
}

function triggerSpringFestivalEffect(x, y, shadowRoot) {
    const festival = getFestivalType();
    if (festival === 'NONE') return;

    let colors = [];
    if (festival === 'CNY') {
        colors = ['#FF0000', '#FFD700', '#FF4500', '#DC143C', '#FFFF00'];
    } else if (festival === 'XMAS') {
        colors = ['#FF0000', '#228B22', '#FFD700', '#FFFFFF', '#006400'];
    }

    const activeColors = [];
    for (let i = 0; i < 3; i++) {
        activeColors.push(colors[Math.floor(Math.random() * colors.length)]);
    }

    const particleCount = 20 + Math.floor(Math.random() * 21);
    const fragment = document.createDocumentFragment();

    for (let i = 0; i < particleCount; i++) {
        const p = document.createElement('div');
        const size = 4 + Math.random() * 3;
        const color = activeColors[Math.floor(Math.random() * activeColors.length)];

        p.style.cssText = `
            position: fixed;
            left: ${x}px;
            top: ${y}px;
            width: ${size}px;
            height: ${size}px;
            background-color: ${color};
            border-radius: 50%;
            pointer-events: none;
            z-index: 2147483647;
            box-shadow: 0 0 6px ${color};
            will-change: transform, opacity;
        `;

        const angle = Math.random() * Math.PI * 2;
        const speed = 2 + Math.random() * 5;
        let vx = Math.cos(angle) * speed;
        let vy = Math.sin(angle) * speed;
        let opacity = 1.0;
        const gravity = 0.2 + Math.random() * 0.1;
        const friction = 0.96;
        const decay = 0.01 + Math.random() * 0.02;

        let posX = x;
        let posY = y;

        const animate = () => {
            if (opacity <= 0) {
                p.remove();
                return;
            }
            vx *= friction;
            vy *= friction;
            vy += gravity;
            posX += vx;
            posY += vy;
            opacity -= decay;

            p.style.transform = `translate(${posX - x}px, ${posY - y}px)`;
            p.style.opacity = opacity;
            requestAnimationFrame(animate);
        };

        fragment.appendChild(p);
        requestAnimationFrame(animate);
    }
    shadowRoot.appendChild(fragment);
}

function getSpringFestivalToastText() {
    const festival = getFestivalType();
    if (festival === 'CNY') {
        return t('festival_cny');
    } else if (festival === 'XMAS') {
        return t('festival_xmas');
    }
    return t('toast_copied');
}

let _visibilityChangeHandler = null;

function registerVisibilityCleanup() {
    if (_visibilityChangeHandler) return;
    _visibilityChangeHandler = async () => {
        if (document.visibilityState === 'hidden') {
            if (getConfig('enablePaste')) {
                await safeSetValue('smart_paste_cache', { text: '', timestamp: 0 });
            }
            unregisterVisibilityCleanup();
        }
    };
    document.addEventListener('visibilitychange', _visibilityChangeHandler, false);
}

function unregisterVisibilityCleanup() {
    if (_visibilityChangeHandler) {
        document.removeEventListener('visibilitychange', _visibilityChangeHandler, false);
        _visibilityChangeHandler = null;
    }
}

(function () {
    'use strict';

    (async function main() {
        try {
            await initConfiguration();

            await initDefaultSearchEngine();

            registerMenus();

            applySavedBlockingRules();

            const handleWheelZoom = (e) => {
                if (!e.ctrlKey || !isUnlockMode) return;

                const hotkey = getConfig('unlockHotkey') || '';
                const isCtrlConfigured = hotkey.includes('Control') || hotkey.toLowerCase() === 'ctrl';

                if (isCtrlConfigured) {
                    e.preventDefault();
                    e.stopPropagation();
                    window.scrollBy({
                        top: e.deltaY,
                        behavior: 'auto'
                    });
                }
            };
            window.addEventListener('wheel', handleWheelZoom, { passive: false, capture: true });

            document.addEventListener('mouseup', handleSelectionMouseUp, false);
            document.addEventListener('mouseup', handleInputPasteMouseUp, true);
            document.addEventListener('mousedown', handleGlobalMouseDown, false);
            document.addEventListener('contextmenu', handleContextMenu, true);
            window.addEventListener('scroll', handleResizeOrScroll, { passive: true });
            window.addEventListener('resize', handleResizeOrScroll, { passive: true });
            document.addEventListener('keydown', handleKeydownHideUI, true);

            if (window.name !== PREVIEW_WIN_NAME) {
                document.addEventListener('dragstart', handleLinkDragStart, false);
                document.addEventListener('dragend', handleLinkDragEnd, false);
            }

        } catch (e) {
        }
    })();
})();