幕布MubuPlus v3.7.1

(同步搜索框+搜索历史面板+选中快速筛选+悬停复制标签+保留软换行和颜色中转粘贴-复制剪切) v3.6.0

目前为 2025-04-17 提交的版本。查看 最新版本

// ==UserScript==
// @name         幕布MubuPlus v3.7.1
// @namespace    http://tampermonkey.net/
// @version      3.7.1 // Increment version for new feature
// @author       Yeeel (Modified based on request)
// @match        *://mubu.com/*
// @match        *://*.mubu.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// @icon         https://mubu.com/favicon.ico
// @license      MIT
// @description (同步搜索框+搜索历史面板+选中快速筛选+悬停复制标签+保留软换行和颜色中转粘贴-复制剪切) v3.6.0
// ==/UserScript==

(function() {
    'use strict';
    console.log('[Mubu Combined Helper v3.7.0 优化历史高亮与导航 + 推开内容] 脚本加载'); // Updated log message

    // --- [ ☆ 功能开关 (默认值) ☆ ] ---
    const FEATURES = {
        syncSearchBox: { enabled: true, label: '同步搜索框' },
        historyPanel: { enabled: true, label: '搜索历史面板' },
        pushContent: { enabled: true, label: '推开右侧文本' }, // <-- 新增功能开关 (默认关闭)
        selectSearchPopup: { enabled: true, label: '选中快速筛选' }, // "筛选"按钮
        copyTagOnHover: { enabled: false, label: '悬停复制标签' },
        transferPasteCopy: { enabled: false, label: '中转粘贴-复制' }, // "复制"按钮
        transferPasteCut: { enabled: false, label: '中转粘贴-剪切' },  // "剪切"按钮
    };

    // --- [ ☆ 运行时功能状态 ☆ ] ---
    let runtimeFeatureState = {};
    for (const key in FEATURES) {
        runtimeFeatureState[key] = FEATURES[key].enabled;
    }
    const isFeatureEnabled = (key) => !!runtimeFeatureState[key];

    // --- [ ☆ 配置项 ☆ ] ---
    const config = {
        cacheTTL: 3000,
        initDelay: 2000,
        selectors: {
            originalInput: 'input[placeholder="搜索关键词"]:not([disabled])',
            domObserverTarget: 'div.search-wrap',
            copyTagParentContainer: 'div.outliner-page',
            // pushContentTarget: '#js-outliner', // Defined within pushContent below
        },
        sync: {
            historySize: 30,
            mutationDebounce: 5,
            throttleTime: 50,
            activeItemBgColor: '#e9e8f9', // 你可以自定义高亮颜色
            topBarId: 'custom-search-sync-container-v35', // Keep v35 for now unless specific conflict
            historyPanelId: 'search-history-panel-v35',
            historyListId: 'search-history-list-v35',
        },
        select: { // 用于 "筛选" 按钮
            popupId: 'mubu-select-search-popup-v35',
            popupText: '🔍',
            popupAboveGap: 5, // 按钮组距离光标上方的距离
            fallbackWidth: 35,
            fallbackHeight: 22,
            popupAppearDelay: 50,
        },
        copyTag: {
            popupId: 'mubu-copy-tag-popup-hover-v35',
            feedbackId: 'mubu-copy-tag-feedback-v35',
            copyIcon: '📋',
            copiedText: '✅ 已复制',
            popupMarginBottom: 0,
            hoverDelay: 10,
            hideDelay: 50,
            copiedMessageDuration: 500,
            tagSelector: 'span.tag',
            popupFallbackWidth: 25,
            popupFallbackHeight: 18,
            feedbackFallbackWidth: 60,
            feedbackFallbackHeight: 18,
        },
        transferPaste: { // 用于 "复制" 和 "剪切" 按钮
            editorContainerSelector: '#js-outliner',
            triggerButtonId: 'mu-transfer-copy-button-v35', // 复制按钮 ID
            cutButtonId: 'mu-transfer-cut-button-v35',     // 剪切按钮 ID
            pasteButtonId: 'mu-transfer-paste-button-v35',   // 粘贴按钮 ID (独立逻辑)
            triggerButtonText: '📄',
            cutButtonText: '✂️',
            pasteButtonText: '📝',
            buttonHorizontalGap: 2, // 按钮水平间距
            cssPrefix: 'mu-transfer-paste-v35-',
            btnBaseClass: 'btn-base',
            btnCopyClass: 'btn-copy',
            btnCutClass: 'btn-cut',
            btnPasteClass: 'btn-paste',
             buttonBaseStyleInline: {
                 position: 'absolute', zIndex: '29998', top: '0', left: '0',
                 opacity: '0', display: 'none', visibility: 'hidden',
             },
            initWaitMaxRetries: 15,
            initWaitRetryInterval: 700,
            buttonFallbackWidth: 35,
            buttonFallbackHeight: 22,
            buttonsAppearDelay: 50,
        },
        togglePanel: {
            panelId: 'mubu-helper-toggle-panel-v35',
            triggerId: 'mubu-helper-toggle-trigger-v35',
            panelWidth: 160,
            triggerWidth: 20,
            triggerHeight: 230,
            hideDelay: 100,
        },
        // --- [ ☆ 推开内容配置 ☆ ] --- // <-- 新增配置区域
        pushContent: {
            // enabled: false, // Handled by FEATURES
            // ☆☆☆ 在这里修改推开的距离 (像素) ☆☆☆
            pushMarginRight: 90, // 原 125 + 15 = 140, 减少 10px -> 130px
            contentSelector: '#js-outliner', // 目标元素的选择器
            pushClass: 'mu-content-pushed-v37', // 添加到目标元素上的 CSS 类名 (version bump)
            transitionDuration: '0.1s' // 过渡动画时间
        }
        // --- [ ☆ 配置项结束 ☆ ] ---
    };
    const BUTTON_GAP = config.transferPaste.buttonHorizontalGap;


    // --- [ ☆ rAF 样式批量处理 ☆ ] ---
    let styleUpdateQueue = [];
    let isRafScheduled = false;
    function processStyleUpdates() {
        const tasksToProcess = [...styleUpdateQueue];
        styleUpdateQueue = [];
        tasksToProcess.forEach(task => {
            if (task.element && task.element.isConnected) {
                try { Object.assign(task.element.style, task.styles); } catch (e) { console.warn('[Mubu Helper] Style apply err:', e, task.element); }
            }
        });
        isRafScheduled = false;
    }
    function scheduleStyleUpdate(element, styles) {
        if (!element) return;
        styleUpdateQueue.push({ element, styles });
        if (!isRafScheduled) {
            isRafScheduled = true;
            requestAnimationFrame(processStyleUpdates);
        }
    }

    // --- [ ☆ ResizeObserver 尺寸缓存 ☆ ] ---
    const elementDimensionsCache = new WeakMap();
    const elementObserverMap = new Map();
    const resizeObserverCallback = (entries) => {
        for (let entry of entries) {
            let width = 0, height = 0;
            if (entry.borderBoxSize?.length > 0) { width = entry.borderBoxSize[0].inlineSize; height = entry.borderBoxSize[0].blockSize; }
            else if (entry.contentRect) { width = entry.contentRect.width; height = entry.contentRect.height; }
            if (width > 0 && height > 0) { elementDimensionsCache.set(entry.target, { width, height }); }
            else if (entry.target.offsetWidth > 0 && entry.target.offsetHeight > 0) { width = entry.target.offsetWidth; height = entry.target.offsetHeight; elementDimensionsCache.set(entry.target, { width, height }); }
        }
    };
    const observerInstance = new ResizeObserver(resizeObserverCallback);
    function observeElementResize(element) {
        if (!element || elementObserverMap.has(element)) return;
        try { observerInstance.observe(element); elementObserverMap.set(element, observerInstance); } catch (e) { console.error('[Mubu Helper] RO observe err:', e, element); }
    }
    function unobserveElementResize(element) {
        if (!element || !elementObserverMap.has(element)) return;
        try { observerInstance.unobserve(element); elementObserverMap.delete(element); } catch (e) { console.error('[Mubu Helper] RO unobserve err:', e, element); }
    }

    // --- [ ☆ 内部状态与工具函数 ☆ ] ---
    let originalInput = null, lastSyncedValue = null, isSyncing = false, domObserver = null, customInput = null;
    let originalInputSyncHandler = null, originalInputHistoryHandler = null;
    let topBarControls = { container: null, input: null, prevBtn: null, nextBtn: null, clearBtn: null };
    let historyPanel = null, historyListElement = null, activeHistoryItemElement = null; // activeHistoryItemElement 存储由点击或导航选中的项
    let popupElement = null;
    let currentSelectedText = '';
    let ct_copyPopupElement = null, ct_feedbackElement = null, ct_currentHoveredTag = null, ct_currentTagText = '';
    let ct_showTimeout = null, ct_hideTimeout = null, ct_feedbackTimeout = null, ct_listenerTarget = null;
    let tp_editorContainer = null, tp_storedHTML = '', tp_storedText = '', tp_ctrlApressed = false, tp_listenersAttached = false;
    let togglePanelElement = null, toggleTriggerElement = null, togglePanelHideTimeout = null;
    const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
    const inputEvent = new Event('input', { bubbles: true, cancelable: true });
    const debounce = (fn, delay) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn.apply(this, a), delay); }; };
    const throttle = (fn, delay) => { let l=0, t; return (...a) => { const n=performance.now(); clearTimeout(t); if(n-l>=delay){requestAnimationFrame(()=>fn.apply(this,a)); l=n;} else {t=setTimeout(()=>{requestAnimationFrame(()=>fn.apply(this,a)); l=performance.now();},delay-(n-l));}};};
    const optimizedFindSearchBox = (()=>{let c=null,l=0;return()=>{const n=performance.now();if(c&&c.isConnected&&(n-l<config.cacheTTL)){return c;}c=null;try{c=document.querySelector(config.selectors.originalInput);}catch(e){c=null;}l=n;return c;}})();
    const docBody = document.body;
    const historyNavPrevListener = () => handleHistoryNavigation(-1);
    const historyNavNextListener = () => handleHistoryNavigation(1);
    const clearBtnListener = () => handleClear();
    const customInputListener = () => handleCustomInputChange();
    const mouseDownPopupListener = (e) => handleMouseDownPopup(e);
    const mouseUpPopupListener = (e) => handleMouseUpSelectionEnd(e);
    const isHistoryTrackingNeeded = () => isFeatureEnabled('syncSearchBox') || isFeatureEnabled('selectSearchPopup') || isFeatureEnabled('historyPanel');

    // 历史记录管理器模块
    const historyManager = (() => {
        const history = new Array(config.sync.historySize);
        let writeIndex = 0, count = 0, navIndex = -1; // navIndex: -1 表示未通过导航/点击选中,>=0 表示选中的逻辑索引
        const add = (value) => {
            if (!isHistoryTrackingNeeded()) return false;
            const term = String(value).trim(); if (!term) return false;
            const lastIdx = (writeIndex - 1 + config.sync.historySize) % config.sync.historySize;
            if (count > 0 && history[lastIdx] === term) { return false; }
            history[writeIndex] = term; writeIndex = (writeIndex + 1) % config.sync.historySize;
            count = Math.min(count + 1, config.sync.historySize); navIndex = -1; // 添加新项后,重置导航索引
            return true;
        };
        const get = (logicalIndex) => { if (!isHistoryTrackingNeeded() || logicalIndex < 0 || logicalIndex >= count) return null; const physicalIndex = (writeIndex - count + logicalIndex + config.sync.historySize) % config.sync.historySize; return history[physicalIndex]; };
        const size = () => isHistoryTrackingNeeded() ? count : 0;
        const getCurrentIndex = () => navIndex; const setCurrentIndex = (index) => { if (isHistoryTrackingNeeded()) navIndex = index; }; const resetIndexToCurrent = () => { if (isHistoryTrackingNeeded()) navIndex = -1; };

        // --- v3.6.2 修改 updatePanel - 解决重复高亮问题 ---
        const updatePanel = () => {
            if (!isFeatureEnabled('historyPanel') || !historyPanel || !historyListElement) return;
            const scrollTop = historyListElement.scrollTop; historyListElement.innerHTML = '';
            const numItems = historyManager.size(); if (numItems === 0) return;

            const currentNavIndex = historyManager.getCurrentIndex();
            let newlyActiveElement = null; // 记录由导航/点击产生的 active 元素
            let matchFoundForLastSyncedValue = false; // v3.6.2 新增:标记是否已为当前搜索词找到高亮

            const fragment = document.createDocumentFragment();
            for (let i = 0; i < numItems; i++) {
                const logicalIndex = numItems - 1 - i; // 从最新到最旧
                const term = historyManager.get(logicalIndex);
                if (term) {
                    const li = document.createElement('li'); li.className = 'search-history-item';
                    li.textContent = term; li.title = term; li.dataset.term = term; li.dataset.historyIndex = logicalIndex;

                    // --- 高亮逻辑 ---
                    if (logicalIndex === currentNavIndex) {
                        // 1. 优先:由导航或点击选中的项
                        li.classList.add('search-history-item--active');
                        newlyActiveElement = li;
                        matchFoundForLastSyncedValue = true; // 手动选中的也算找到了匹配(阻止下面条件再次匹配)
                    } else if (currentNavIndex === -1 && !matchFoundForLastSyncedValue && term && lastSyncedValue && term === lastSyncedValue) {
                        // 2. 其次:无手动选中项时,匹配当前搜索词,且是找到的第一个(最新)匹配项
                        li.classList.add('search-history-item--active');
                        matchFoundForLastSyncedValue = true; // 标记已找到,后续重复项不再高亮
                        // 注意:这里 *不* 更新 newlyActiveElement
                    }
                    // --- 高亮逻辑结束 ---

                    fragment.appendChild(li);
                }
            }
            historyListElement.appendChild(fragment);
            try { historyListElement.scrollTop = scrollTop; } catch (e) {/* ignore */}

            activeHistoryItemElement = newlyActiveElement;
        };
        // --- v3.6.2 修改结束 ---

        return { add, get, size, getCurrentIndex, setCurrentIndex, resetIndexToCurrent, updatePanel };
    })();

    // --- [ ☆ 同步与历史记录核心逻辑 ☆ ] ---
    const runSynced = (action) => { if (!isFeatureEnabled('syncSearchBox') || isSyncing) return; isSyncing = true; try { action(); } finally { queueMicrotask(() => { isSyncing = false; }); } };

    const updateCustomInputAndAddHistory = (newValue, source = 'unknown') => {
        if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== newValue) { customInput.value = newValue; }
        const oldValue = lastSyncedValue; const valueChanged = newValue !== oldValue;
        lastSyncedValue = newValue; // 更新最新值
        let historyChanged = false; if (isHistoryTrackingNeeded()) { historyChanged = historyManager.add(newValue); } // add内部会重置navIndex=-1(如果真添加了)
        // 如果值改变了,且不是来自导航操作,确保重置导航索引(即使add没添加,也要重置)
        if (valueChanged && source !== 'navigation' && isHistoryTrackingNeeded() && isFeatureEnabled('historyPanel')) { historyManager.resetIndexToCurrent(); }
        // 更新历史面板
        if (isFeatureEnabled('historyPanel') && (historyChanged || valueChanged)) { historyManager.updatePanel(); }
    };

    const findAndSetupDebounced = debounce(() => {
        if (!isFeatureEnabled('syncSearchBox') && !isFeatureEnabled('historyPanel')) return;
        const foundInput = optimizedFindSearchBox();
        if (foundInput) {
            const currentValue = foundInput.value;
            if (foundInput !== originalInput) {
                teardownInputListeners(originalInput); originalInput = foundInput; setupInputListeners(originalInput);
                updateCustomInputAndAddHistory(currentValue, 'observer_init');
                if (isFeatureEnabled('syncSearchBox') && customInput) customInput.value = lastSyncedValue;
            } else if (currentValue !== lastSyncedValue && !isSyncing) {
                 updateCustomInputAndAddHistory(currentValue, 'observer_external');
            }
        } else if (!foundInput && originalInput) { teardownInputListeners(originalInput); originalInput = null; lastSyncedValue = null; if (isFeatureEnabled('syncSearchBox')) isSyncing = false; }
    }, config.sync.mutationDebounce);

    // --- [ ☆ 中转粘贴 (Transfer Paste) 相关 ☆ ] ---
    const tp_triggerButtonRef = { element: null }; const tp_cutButtonRef = { element: null }; const tp_pasteButtonRef = { element: null };
    const tp_hidePasteButton = () => { if (tp_pasteButtonRef.element) { scheduleStyleUpdate(tp_pasteButtonRef.element, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (tp_pasteButtonRef.element?.style.opacity === '0') scheduleStyleUpdate(tp_pasteButtonRef.element, { display: 'none' }); }, 150); } }
    function getCursorRect(selection) { if (!selection || !selection.focusNode || selection.rangeCount === 0) { return null; } const range = document.createRange(); try { range.setStart(selection.focusNode, selection.focusOffset); range.setEnd(selection.focusNode, selection.focusOffset); const rect = range.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0 && selection.toString().trim().length > 0) { const mainRange = selection.getRangeAt(0); const rects = mainRange.getClientRects(); return rects.length > 0 ? rects[rects.length - 1] : mainRange.getBoundingClientRect(); } return rect; } catch (e) { try { return selection.getRangeAt(0).getBoundingClientRect(); } catch { return null; } } }
    function tp_captureSelectionAndStore() { const selection = window.getSelection(); if (!selection || selection.isCollapsed || selection.rangeCount === 0) return false; try { const range = selection.getRangeAt(0); const tempDiv = document.createElement('div'); tempDiv.appendChild(range.cloneContents()); tp_storedHTML = tempDiv.innerHTML; tp_storedText = selection.toString(); return true; } catch (e) { console.error('[Mubu Helper TP] Capture err:', e); tp_storedHTML = ''; tp_storedText = ''; return false; } }
    function tp_isElementEditable(element) { if (!element) return false; if (element instanceof Element && element.closest) { if (element.closest('[contenteditable="true"], .mm-editor')) { return true; } } if (element instanceof HTMLElement && ['INPUT', 'TEXTAREA'].includes(element.tagName)) { return !element.readOnly && !element.disabled; } return false; }
    function tp_createButton(id, text, buttonClass, clickHandler) { const tpConfig = config.transferPaste; let existing = document.getElementById(id); if (existing) { existing.textContent = text; existing.removeEventListener('click', existing.__clickHandler__); existing.removeEventListener('mousedown', existing.__mousedownHandler__); } else { existing = document.createElement('button'); existing.id = id; existing.textContent = text; Object.assign(existing.style, tpConfig.buttonBaseStyleInline); document.body.appendChild(existing); } existing.className = ''; existing.classList.add(tpConfig.cssPrefix + tpConfig.btnBaseClass, tpConfig.cssPrefix + buttonClass); const mousedownHandler = (e) => e.stopPropagation(); const clickHandlerWrapper = (e) => { e.stopPropagation(); clickHandler(existing); }; existing.addEventListener('mousedown', mousedownHandler); existing.addEventListener('click', clickHandlerWrapper); existing.__clickHandler__ = clickHandlerWrapper; existing.__mousedownHandler__ = mousedownHandler; observeElementResize(existing); return existing; }
    function tp_showPasteButton(event) { const tpConfig = config.transferPaste; hideSelectionActionButtons(); tp_hidePasteButton(); if (!tp_storedHTML && !tp_storedText) return; const targetElement = event.target instanceof Node ? event.target : document.elementFromPoint(event.clientX, event.clientY); if (!tp_isElementEditable(targetElement)) { return; } const positionRect = { top: event.clientY, left: event.clientX, bottom: event.clientY, right: event.clientX, width: 0, height: 0 }; const positionButtonStandalone = (buttonElement, targetRect) => { try { if (!targetRect || !buttonElement || !buttonElement.isConnected) return false; const dims = elementDimensionsCache.get(buttonElement); const btnW = dims?.width || tpConfig.buttonFallbackWidth; const btnH = dims?.height || tpConfig.buttonFallbackHeight; const vpW = window.innerWidth; const scrollY = window.pageYOffset; const scrollX = window.pageXOffset; let top = scrollY + targetRect.top + 10; let left = scrollX + targetRect.left - btnW / 2; top = Math.max(scrollY + 5, top); left = Math.max(scrollX + 5, Math.min(left, scrollX + vpW - btnW - 5)); scheduleStyleUpdate(buttonElement, { transform: `translate(${left.toFixed(1)}px, ${top.toFixed(1)}px)`, display: 'inline-block', opacity: '1', visibility: 'visible' }); return true; } catch (e) { console.error('[Mubu Helper TP] Pos paste btn err:', e, buttonElement); if (buttonElement) scheduleStyleUpdate(buttonElement, { display: 'none', opacity: '0', visibility: 'hidden' }); return false; } }; tp_pasteButtonRef.element = tp_createButton( tpConfig.pasteButtonId, tpConfig.pasteButtonText, tpConfig.btnPasteClass, (button) => { const currentSel = window.getSelection(); const range = currentSel?.rangeCount > 0 ? currentSel.getRangeAt(0) : null; let pasteTarget = null; if (range) { pasteTarget = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement; if (!tp_isElementEditable(pasteTarget)) { pasteTarget = document.elementFromPoint(event.clientX, event.clientY); } } else { pasteTarget = document.elementFromPoint(event.clientX, event.clientY); } if (!tp_isElementEditable(pasteTarget)) { alert('无法粘贴:位置不可编辑。'); tp_hidePasteButton(); return; } try { let success = false; if (tp_storedHTML && document.queryCommandSupported('insertHTML')) { try { if (range) { currentSel.removeAllRanges(); currentSel.addRange(range); } else { if (pasteTarget instanceof HTMLElement) pasteTarget.focus(); } if (document.execCommand('insertHTML', false, tp_storedHTML)) { success = true; } else { console.warn('[Mubu TP] insertHTML failed.'); } } catch (e) { console.error('[Mubu TP] insertHTML err:', e); } } if (!success && tp_storedText && document.queryCommandSupported('insertText')) { try { if (range) { currentSel.removeAllRanges(); currentSel.addRange(range); } else { if (pasteTarget instanceof HTMLElement) pasteTarget.focus(); } if (document.execCommand('insertText', false, tp_storedText)) { success = true; } else { console.warn('[Mubu TP] insertText failed.'); } } catch (e) { console.error('[Mubu TP] insertText err:', e); } } if (!success) { console.error('[Mubu TP] Paste failed using execCommand.'); alert('粘贴失败。请尝试手动 Ctrl+V。'); } else { tp_storedHTML = ''; tp_storedText = ''; } } catch (e) { console.error('[Mubu TP] Paste process err:', e); alert(`粘贴时出错: ${e.message}`); } finally { tp_hidePasteButton(); } } ); if (!positionButtonStandalone(tp_pasteButtonRef.element, positionRect)) { tp_hidePasteButton(); } }

    // --- [ ☆ 复制标签 (Copy Tag) 相关 ☆ ] ---
    function calculateTransformForPopup(element, targetRect, marginBottom = 0) { if (!element || !targetRect || !element.isConnected) return null; const ctConfig = config.copyTag; const dims = elementDimensionsCache.get(element); let W, H; if (element === ct_copyPopupElement) { W = dims?.width || ctConfig.popupFallbackWidth; H = dims?.height || ctConfig.popupFallbackHeight; } else if (element === ct_feedbackElement) { W = dims?.width || ctConfig.feedbackFallbackWidth; H = dims?.height || ctConfig.feedbackFallbackHeight; } else { W = dims?.width || 30; H = dims?.height || 20; } const x = window.pageXOffset; const y = window.pageYOffset; const targetCenterX = targetRect.left + targetRect.width / 2; const top = y + targetRect.top - H - marginBottom; const left = x + targetCenterX - W / 2; return `translate(${left.toFixed(1)}px, ${top.toFixed(1)}px)`; }
    function ct_showCopyPopup(tagElement){ if (!isFeatureEnabled('copyTagOnHover')) return; ct_createElements(); if (!ct_copyPopupElement) return; if (!tagElement || !ct_copyPopupElement.isConnected) return; const tagText = tagElement.textContent?.trim(); if (!tagText) return; ct_currentTagText = tagText; const targetRect = tagElement.getBoundingClientRect(); const transform = calculateTransformForPopup(ct_copyPopupElement, targetRect, config.copyTag.popupMarginBottom); if (transform) { scheduleStyleUpdate(ct_copyPopupElement, { transform: transform, display: 'block', opacity: '1', visibility: 'visible' }); } else { console.warn("[CT] show copy popup: no transform"); scheduleStyleUpdate(ct_copyPopupElement, { transform: 'translate(0px, -20px)', display: 'block', opacity: '1', visibility: 'visible' }); } }
    function ct_hideCopyPopupImmediately(clearState = true){ clearTimeout(ct_showTimeout); ct_showTimeout = null; clearTimeout(ct_hideTimeout); ct_hideTimeout = null; if (ct_copyPopupElement?.isConnected) { scheduleStyleUpdate(ct_copyPopupElement, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (ct_copyPopupElement?.style.opacity === '0') scheduleStyleUpdate(ct_copyPopupElement, { display: 'none' }); }, 150); } if (clearState) { ct_currentHoveredTag = null; ct_currentTagText = ''; } }
    function ct_scheduleHidePopup() { clearTimeout(ct_showTimeout); ct_showTimeout = null; clearTimeout(ct_hideTimeout); ct_hideTimeout = setTimeout(() => { const tagHover = ct_currentHoveredTag?.matches(':hover'); const popupHover = ct_copyPopupElement?.matches(':hover'); if (!tagHover && !popupHover) { ct_hideCopyPopupImmediately(true); } ct_hideTimeout = null; }, config.copyTag.hideDelay); }
    function ct_showFeedbackIndicator(tagElement){ if (!isFeatureEnabled('copyTagOnHover')) return; ct_createElements(); if (!ct_feedbackElement) { console.error("[CT] Feedback element not available."); return; } if (!tagElement || !ct_feedbackElement.isConnected) return; const duration = config.copyTag.copiedMessageDuration; clearTimeout(ct_feedbackTimeout); const targetRect = tagElement.getBoundingClientRect(); const transform = calculateTransformForPopup(ct_feedbackElement, targetRect, config.copyTag.popupMarginBottom); if (transform) { scheduleStyleUpdate(ct_feedbackElement, { transform: transform, display: 'block', opacity: '1', visibility: 'visible' }); } else { console.warn("[CT] feedback: no transform"); scheduleStyleUpdate(ct_feedbackElement, { transform: 'translate(0px, -20px)', display: 'block', opacity: '1', visibility: 'visible' }); } ct_feedbackTimeout = setTimeout(ct_hideFeedbackIndicator, duration); }
    function ct_hideFeedbackIndicator(){ if (!ct_feedbackElement?.isConnected) return; scheduleStyleUpdate(ct_feedbackElement, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (ct_feedbackElement?.style.opacity === '0') scheduleStyleUpdate(ct_feedbackElement, { display: 'none' }); }, 150); ct_feedbackTimeout = null; }

    // --- [ ☆ UI 创建函数 ☆ ] ---
    function ct_createElements(){ if (!isFeatureEnabled('copyTagOnHover')) return; const ctConf = config.copyTag; const baseStylePopup = { position: 'absolute', top: '0', left: '0', zIndex: '10010', display: 'none', opacity: '0', visibility: 'hidden' }; const baseStyleFeedback = { position: 'absolute', top: '0', left: '0', zIndex: '10011', display: 'none', opacity: '0', visibility: 'hidden', pointerEvents: 'none' }; let existingPopup = document.getElementById(ctConf.popupId); if (!ct_copyPopupElement && existingPopup) { ct_copyPopupElement = existingPopup; if (!elementObserverMap.has(ct_copyPopupElement)) observeElementResize(ct_copyPopupElement); } else if (!ct_copyPopupElement && !existingPopup) { const copyPopup = document.createElement('button'); copyPopup.id = ctConf.popupId; copyPopup.textContent = ctConf.copyIcon; Object.assign(copyPopup.style, baseStylePopup); copyPopup.addEventListener('click', ct_handleCopyButtonClick); copyPopup.addEventListener('mouseenter', () => { clearTimeout(ct_hideTimeout); ct_hideTimeout = null; }); copyPopup.addEventListener('mouseleave', ct_scheduleHidePopup); copyPopup.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); }); document.body.appendChild(copyPopup); ct_copyPopupElement = copyPopup; observeElementResize(ct_copyPopupElement); } let existingFeedback = document.getElementById(ctConf.feedbackId); if (!ct_feedbackElement && existingFeedback) { ct_feedbackElement = existingFeedback; if (!elementObserverMap.has(ct_feedbackElement)) observeElementResize(ct_feedbackElement); } else if (!ct_feedbackElement && !existingFeedback) { const feedback = document.createElement('div'); feedback.id = ctConf.feedbackId; feedback.textContent = ctConf.copiedText; Object.assign(feedback.style, baseStyleFeedback); document.body.appendChild(feedback); ct_feedbackElement = feedback; observeElementResize(ct_feedbackElement); } }
    function createControlPanel(){ if(document.getElementById(config.sync.topBarId)) return topBarControls; try{ const c=document.createElement('div'); c.id=config.sync.topBarId; c.style.display = 'none'; const l=document.createElement('button');l.className='clear-btn';l.textContent='✕';l.title='清空'; const p=document.createElement('button');p.className='history-btn';p.textContent='←';p.title='上条'; const n=document.createElement('button');n.className='history-btn';n.textContent='→';n.title='下条'; const i=document.createElement('input');i.className='custom-search-input';i.type='search';i.placeholder='筛选';i.setAttribute('autocomplete','off'); c.append(l,p,n,i); document.body.appendChild(c); topBarControls={container:c,input:i,prevBtn:p,nextBtn:n,clearBtn:l}; return topBarControls; } catch(e) { console.error('[Mubu] Create top bar err:',e); topBarControls={}; return topBarControls; } }
    function createHistoryPanel(){ if(document.getElementById(config.sync.historyPanelId)) return historyPanel; try{ const p=document.createElement('div'); p.id=config.sync.historyPanelId; p.style.display = 'none'; const l=document.createElement('ul'); l.className='search-history-list'; l.id=config.sync.historyListId; p.appendChild(l); document.body.appendChild(p); historyPanel = p; historyListElement = l; return p; } catch(e) { console.error('[Mubu] Create hist panel err:',e); historyPanel=null; historyListElement=null; return null; } }
    function createSelectPopup(){ if (!isFeatureEnabled('selectSearchPopup')) return; const popupId = config.select.popupId; let existing = document.getElementById(popupId); if (existing) { popupElement = existing; if (!elementObserverMap.has(popupElement)) observeElementResize(popupElement); Object.assign(popupElement.style, { display: 'none', opacity: '0', visibility: 'hidden' }); if (!existing.__clickAttached__) { existing.addEventListener('mousedown', handlePopupClick); existing.addEventListener('click', (e) => e.stopPropagation()); existing.__clickAttached__ = true; } return; } try { const btn = document.createElement('button'); btn.id = popupId; btn.textContent = config.select.popupText; Object.assign(btn.style, { position: 'absolute', top: '0', left: '0', zIndex: '10010', display: 'none', opacity: '0', visibility: 'hidden', }); btn.classList.add('mu-select-popup-btn'); btn.addEventListener('mousedown', handlePopupClick); btn.addEventListener('click', (e) => e.stopPropagation()); btn.__clickAttached__ = true; document.body.appendChild(btn); popupElement = btn; observeElementResize(popupElement); } catch(e) { console.error('[Mubu SS] Create popup err:', e); popupElement = null; } }

    // --- [ ☆ 事件处理函数 ☆ ] ---
    function syncFromOriginal(sourceInput) { if(!isFeatureEnabled('syncSearchBox') || !sourceInput) return; const val=sourceInput.value; if(val===lastSyncedValue)return; runSynced(()=>{updateCustomInputAndAddHistory(val,'sync_from_original');}); }
    function syncToOriginal(options = { updateHistory: true }) { if (!isFeatureEnabled('syncSearchBox') || !originalInput || !customInput || !nativeInputValueSetter) return; const val = customInput.value; runSynced(() => { if (originalInput.value !== val) { nativeInputValueSetter.call(originalInput, val); originalInput.dispatchEvent(inputEvent); } updateCustomInputAndAddHistory(val, 'sync_to_original'); }); }
    function handleOriginalInputForSync(event) { if (!event.isTrusted || !isFeatureEnabled('syncSearchBox') || isSyncing) return; syncFromOriginal(event.target); }
    function handleOriginalInputForHistory(event) { if (!event.isTrusted || isFeatureEnabled('syncSearchBox') || !isFeatureEnabled('historyPanel') || !isHistoryTrackingNeeded() || isSyncing) return; const val = event.target.value; if (val === lastSyncedValue) return; updateCustomInputAndAddHistory(val, 'input_external'); }
    function handleCustomInputChange() { if (!isFeatureEnabled('syncSearchBox') || isSyncing) return; syncToOriginal(); }
    function removeActiveHighlight() { if (!isFeatureEnabled('historyPanel') || !activeHistoryItemElement) return; try { activeHistoryItemElement.classList.remove('search-history-item--active'); } catch (e) { /*...*/ } activeHistoryItemElement = null; historyManager.resetIndexToCurrent(); }
    function handleHistoryListClick(event) { if (!isFeatureEnabled('historyPanel')) return; const item = event.target.closest('.search-history-item'); if (!item) return; const term = item.dataset.term; const idxStr = item.dataset.historyIndex; if (term === undefined || idxStr === undefined) return; const idx = parseInt(idxStr, 10); if (isNaN(idx)) return; if (isHistoryTrackingNeeded()) { historyManager.setCurrentIndex(idx); } const targetInput = optimizedFindSearchBox(); if (targetInput && nativeInputValueSetter) { try { if (targetInput.value !== term) { nativeInputValueSetter.call(targetInput, term); targetInput.dispatchEvent(inputEvent); } targetInput.focus(); lastSyncedValue = term; } catch (error) { console.error("[Mubu HistClick] Input err:", error); } } else { console.warn("[Mubu HistClick] Input not found."); } if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== term) { customInput.value = term; } historyManager.updatePanel(); }

    // --- v3.6.2 修改 handleHistoryNavigation - 解决导航问题 ---
    function handleHistoryNavigation(direction) {
        throttle((dir) => {
            if (!isFeatureEnabled('syncSearchBox') || !originalInput || !customInput || !nativeInputValueSetter || !isHistoryTrackingNeeded()) return;
            const size = historyManager.size(); if (size === 0) return;

            let currentActualIndex = historyManager.getCurrentIndex(); // 当前实际选中的索引 (-1 代表无)
            let referenceIndex = currentActualIndex; // 计算导航的基准索引

            // 如果当前没有手动选中项,并且搜索框里有值,尝试找到该值对应的最新历史记录项作为导航基点
            if (referenceIndex === -1 && lastSyncedValue) {
                let matchedIndex = -1;
                for (let i = 0; i < size; i++) {
                    const logicalIndex = size - 1 - i; // 从最新到最旧查找
                    const term = historyManager.get(logicalIndex);
                    if (term === lastSyncedValue) {
                        matchedIndex = logicalIndex;
                        break; // 找到最新的匹配项
                    }
                }
                if (matchedIndex !== -1) {
                    referenceIndex = matchedIndex; // 将基准设置为匹配项的索引
                }
                // 如果没找到匹配项 (e.g., lastSyncedValue 为空或不在历史中),referenceIndex 保持 -1
            }

            // 根据基准索引和方向计算下一个目标索引
            let nextIdx;
            if (referenceIndex === -1) {
                // 情况1:没有基准项 (未选中任何项,且当前输入值也无匹配)
                if (dir === -1) { // 按 "上一个" (←)
                    nextIdx = size - 1; // 定位到列表中的最新一项 (logical index = size - 1)
                } else { // 按 "下一个" (→)
                    nextIdx = -1; // 保持在 "未选中" 状态
                }
            } else {
                // 情况2:有基准项 (之前选中了某项,或当前输入值匹配了某项)
                nextIdx = referenceIndex + dir; // 直接在基准项上加或减
            }

            // 将计算出的索引限制在有效范围内 [-1, size-1]
            nextIdx = Math.max(-1, Math.min(nextIdx, size - 1));

            // 如果计算出的最终目标索引与导航开始时的实际选中索引相同,则不执行任何操作
            // (例外:如果开始时是 -1,即使最后计算结果还是 -1,也应该继续,因为这可能是从边界导航回来的结果)
            if (nextIdx === currentActualIndex && currentActualIndex !== -1) {
                 // console.log(`[Mubu Nav] No change needed. nextIdx: ${nextIdx}, currentActualIndex: ${currentActualIndex}`);
                return;
            }

            // --- 执行导航 ---
            historyManager.setCurrentIndex(nextIdx); // 更新管理器中的当前索引

            runSynced(() => {
                // 获取导航到的项目的值。如果 nextIdx 是 -1 (导航到了 "未选中" 状态),
                // 则输入框应恢复显示导航开始前的那个搜索词 (lastSyncedValue)。
                const valueBeforeNav = lastSyncedValue; // 保存导航前的值
                const navigatedValue = (nextIdx === -1) ? (valueBeforeNav ?? '') : (historyManager.get(nextIdx) ?? '');

                // 更新顶部输入框 (如果启用)
                if (customInput && customInput.value !== navigatedValue) {
                    customInput.value = navigatedValue;
                }
                // 更新原始搜索框
                if (originalInput && originalInput.value !== navigatedValue) {
                    nativeInputValueSetter.call(originalInput, navigatedValue);
                    originalInput.dispatchEvent(inputEvent);
                }

                // 更新 lastSyncedValue 以反映导航后的状态
                lastSyncedValue = navigatedValue;

                // 更新历史面板的显示 (高亮等)
                if (isFeatureEnabled('historyPanel')) {
                    historyManager.updatePanel();
                }
            });
        }, config.sync.throttleTime)(direction);
    }
    // --- v3.6.2 修改结束 ---

    function handleClear() { if (!isFeatureEnabled('syncSearchBox') || !originalInput || !customInput || !nativeInputValueSetter) return; if (customInput.value === '' && originalInput.value === '') return; if (customInput.value !== '') customInput.value = ''; syncToOriginal({ updateHistory: false }); }
    function setupInputListeners(targetInput){ if (!targetInput){return;} teardownInputListeners(targetInput); if (isFeatureEnabled('syncSearchBox') && customInput){ originalInputSyncHandler = handleOriginalInputForSync; targetInput.addEventListener('input', originalInputSyncHandler, { passive: true }); } else if (isFeatureEnabled('historyPanel') && isHistoryTrackingNeeded()){ originalInputHistoryHandler = handleOriginalInputForHistory; targetInput.addEventListener('input', originalInputHistoryHandler, { passive: true }); } }
    function teardownInputListeners(targetInput){ if (!targetInput) return; if (originalInputSyncHandler){ try { targetInput.removeEventListener('input', originalInputSyncHandler); } catch(e){} } if (originalInputHistoryHandler){ try { targetInput.removeEventListener('input', originalInputHistoryHandler); } catch(e){} } originalInputSyncHandler = null; originalInputHistoryHandler = null; }
    function handlePopupClick(event){ if(!isFeatureEnabled('selectSearchPopup')) return; event.preventDefault(); event.stopPropagation(); const term = currentSelectedText; if(!term){ hideSelectionActionButtons(); return; } const targetInput = optimizedFindSearchBox(); if(targetInput && nativeInputValueSetter){ try{ nativeInputValueSetter.call(targetInput, term); targetInput.dispatchEvent(inputEvent); targetInput.focus(); updateCustomInputAndAddHistory(term, 'select_popup'); } catch(error){ console.error("[Mubu SS] Trigger err:", error); alert(`触发筛选时出错: ${error.message}`); } } else { console.warn("[Mubu SS] Input not found."); alert("未找到搜索框!"); } hideSelectionActionButtons(); }
    function handleMouseDownPopup(event){ const target = event.target; if (target instanceof Node) { const isClickOnActionButton = popupElement?.contains(target) || tp_triggerButtonRef.element?.contains(target) || tp_cutButtonRef.element?.contains(target); const isClickOnToggle = togglePanelElement?.contains(target) || toggleTriggerElement?.contains(target); const isClickOnPasteButton = tp_pasteButtonRef.element?.contains(target); if (!isClickOnActionButton && !isClickOnToggle && !isClickOnPasteButton) { hideSelectionActionButtons(); } if (!isClickOnPasteButton && !isClickOnToggle) { tp_hidePasteButton(); } } else { hideSelectionActionButtons(); tp_hidePasteButton(); } }
    function handleMouseUpSelectionEnd(event){ const target = event.target; if (target instanceof Node) { const isClickOnActionButton = popupElement?.contains(target) || tp_triggerButtonRef.element?.contains(target) || tp_cutButtonRef.element?.contains(target); const isClickOnToggle = togglePanelElement?.contains(target) || toggleTriggerElement?.contains(target); const isClickOnPasteButton = tp_pasteButtonRef.element?.contains(target); if (isClickOnActionButton || isClickOnToggle || isClickOnPasteButton) { return; } } setTimeout(() => { requestAnimationFrame(() => { const selection = window.getSelection(); if (selection && !selection.isCollapsed && selection.toString().trim().length > 0) { const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; if (range) { const containerNode = range.commonAncestorContainer; const isInEditable = containerNode && ((containerNode.nodeType === Node.ELEMENT_NODE && tp_isElementEditable(containerNode)) || (containerNode.nodeType === Node.TEXT_NODE && containerNode.parentElement && tp_isElementEditable(containerNode.parentElement))); if (isInEditable) { console.log('[Mubu Helper] Showing buttons after mouseup (normal selection)'); showSelectionActionButtons(selection, false); } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } }); }, config.select.popupAppearDelay); }
    function hideSelectionActionButtons() { if (popupElement?.isConnected && popupElement.style.visibility !== 'hidden') { scheduleStyleUpdate(popupElement, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (popupElement?.style.opacity === '0') scheduleStyleUpdate(popupElement, { display: 'none' }); }, 150); } if (tp_triggerButtonRef.element?.isConnected && tp_triggerButtonRef.element.style.visibility !== 'hidden') { scheduleStyleUpdate(tp_triggerButtonRef.element, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (tp_triggerButtonRef.element?.style.opacity === '0') scheduleStyleUpdate(tp_triggerButtonRef.element, { display: 'none' }); }, 150); } if (tp_cutButtonRef.element?.isConnected && tp_cutButtonRef.element.style.visibility !== 'hidden') { scheduleStyleUpdate(tp_cutButtonRef.element, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (tp_cutButtonRef.element?.style.opacity === '0') scheduleStyleUpdate(tp_cutButtonRef.element, { display: 'none' }); }, 150); } currentSelectedText = ''; }
    function showSelectionActionButtons(selection, isCtrlA = false) { hideSelectionActionButtons(); tp_hidePasteButton(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; const selectionText = selection.toString().trim(); if (selectionText.length === 0) return; const containerNode = selection.getRangeAt(0).commonAncestorContainer; const isInEditable = containerNode && ((containerNode.nodeType === Node.ELEMENT_NODE && tp_isElementEditable(containerNode)) || (containerNode.nodeType === Node.TEXT_NODE && containerNode.parentElement && tp_isElementEditable(containerNode.parentElement))); if (!isInEditable) return; const buttonOrder = isCtrlA ? ['copy', 'cut'] : ['filter', 'copy', 'cut']; const visibleButtonsData = []; let maxHeight = 0; buttonOrder.forEach(type => { let buttonInfo = null; const shouldAppear = ((type === 'filter' && !isCtrlA && isFeatureEnabled('selectSearchPopup')) || (type === 'copy' && isFeatureEnabled('transferPasteCopy')) || (type === 'cut' && isFeatureEnabled('transferPasteCut'))); if (shouldAppear) { try { if (type === 'filter') { if (!popupElement) createSelectPopup(); if (!popupElement) return; buttonInfo = { type: 'filter', element: popupElement, fallbackW: config.select.fallbackWidth, fallbackH: config.select.fallbackHeight }; currentSelectedText = selectionText; } else if (type === 'copy') { tp_triggerButtonRef.element = tp_createButton( config.transferPaste.triggerButtonId, config.transferPaste.triggerButtonText, config.transferPaste.btnCopyClass, (button) => { if (!tp_captureSelectionAndStore()) { alert('捕获选区失败!'); } hideSelectionActionButtons(); } ); if (!tp_triggerButtonRef.element) return; buttonInfo = { type: 'copy', element: tp_triggerButtonRef.element, fallbackW: config.transferPaste.buttonFallbackWidth, fallbackH: config.transferPaste.buttonFallbackHeight }; } else if (type === 'cut') { tp_cutButtonRef.element = tp_createButton( config.transferPaste.cutButtonId, config.transferPaste.cutButtonText, config.transferPaste.btnCutClass, (button) => { const latestSel = window.getSelection(); if (latestSel && !latestSel.isCollapsed) { if (tp_captureSelectionAndStore()) { try { if (!document.execCommand('delete', false, null)) { console.warn("[Mubu TP] execCommand('delete') failed, fallback."); latestSel.getRangeAt(0).deleteContents(); } } catch (e) { console.error('[Mubu TP] Delete err, fallback:', e); try { latestSel.getRangeAt(0).deleteContents(); } catch (e2) { console.error('[Mubu TP] Fallback err:', e2); alert('剪切删除失败.'); } } } else { alert('捕获失败,无法剪切!'); } } else { alert('选区失效,无法剪切!'); } hideSelectionActionButtons(); } ); if (!tp_cutButtonRef.element) return; buttonInfo = { type: 'cut', element: tp_cutButtonRef.element, fallbackW: config.transferPaste.buttonFallbackWidth, fallbackH: config.transferPaste.buttonFallbackHeight }; } } catch (creationError) { console.error(`[Mubu Helper] Error creating/getting button type ${type}:`, creationError); return; } } if (buttonInfo && buttonInfo.element && buttonInfo.element.isConnected) { observeElementResize(buttonInfo.element); const dims = elementDimensionsCache.get(buttonInfo.element); buttonInfo.width = dims?.width || buttonInfo.fallbackW; buttonInfo.height = dims?.height || buttonInfo.fallbackH; if (buttonInfo.width > 0 && buttonInfo.height > 0) { maxHeight = Math.max(maxHeight, buttonInfo.height); visibleButtonsData.push(buttonInfo); } else { const ow = buttonInfo.element.offsetWidth; const oh = buttonInfo.element.offsetHeight; if (ow > 0 && oh > 0) { buttonInfo.width = ow; buttonInfo.height = oh; elementDimensionsCache.set(buttonInfo.element, { width: ow, height: oh }); maxHeight = Math.max(maxHeight, buttonInfo.height); visibleButtonsData.push(buttonInfo); } else { console.warn(`[Mubu Helper] Invalid or zero dimensions for button type: ${buttonInfo.type}`, buttonInfo.element); } } } }); if (visibleButtonsData.length === 0) return; const targetRect = getCursorRect(selection); if (!targetRect || (targetRect.width === 0 && targetRect.height === 0 && selectionText.length === 0) ) { hideSelectionActionButtons(); return; } const totalWidth = visibleButtonsData.reduce((sum, btn) => sum + btn.width, 0) + Math.max(0, visibleButtonsData.length - 1) * BUTTON_GAP; const scrollY = window.pageYOffset; const scrollX = window.pageXOffset; const vpW = window.innerWidth; const groupTop = Math.max(scrollY + 5, scrollY + targetRect.top - maxHeight - config.select.popupAboveGap); const selectionCenterX = targetRect.left + targetRect.width / 2; let groupLeftStart = scrollX + selectionCenterX - totalWidth / 2; groupLeftStart = Math.max(scrollX + 5, groupLeftStart); if (groupLeftStart + totalWidth > scrollX + vpW - 5) { groupLeftStart = scrollX + vpW - totalWidth - 5; groupLeftStart = Math.max(scrollX + 5, groupLeftStart); } let currentLeftOffset = 0; visibleButtonsData.forEach((buttonInfo, index) => { const currentButtonLeft = groupLeftStart + currentLeftOffset; if (!buttonInfo.element.isConnected) { try { docBody.appendChild(buttonInfo.element); observeElementResize(buttonInfo.element); } catch(e) { console.error("Error re-appending button:", e); return; } } scheduleStyleUpdate(buttonInfo.element, { transform: `translate(${currentButtonLeft.toFixed(1)}px, ${groupTop.toFixed(1)}px)`, display: 'inline-block', opacity: '1', visibility: 'visible' }); currentLeftOffset += buttonInfo.width + (index < visibleButtonsData.length - 1 ? BUTTON_GAP : 0); }); }
    async function ct_handleCopyButtonClick(event) { if (!isFeatureEnabled('copyTagOnHover')) return; event.stopPropagation(); event.preventDefault(); if (!ct_currentTagText || !ct_copyPopupElement || !ct_currentHoveredTag) return; const text = " " + ct_currentTagText; try { await navigator.clipboard.writeText(text); ct_showFeedbackIndicator(ct_currentHoveredTag); ct_hideCopyPopupImmediately(false); } catch (err) { console.error("[CT] Clipboard err:", err); alert(`复制失败: ${err.message}`); } }
    function ct_handleMouseOver(event) { if (!isFeatureEnabled('copyTagOnHover')) return; if (!(event.target instanceof Element)) { return; } const relevant = event.target.closest(`${config.copyTag.tagSelector}, #${config.copyTag.popupId}`); if (!relevant) { if (ct_currentHoveredTag) { ct_scheduleHidePopup(); } return; } ct_createElements(); if (!ct_copyPopupElement) { console.warn("[CT] Copy popup element missing."); return; } const tagEl = relevant.matches(config.copyTag.tagSelector) ? relevant : null; const isOverPopup = relevant === ct_copyPopupElement; if (tagEl) { if (tagEl === ct_currentHoveredTag) { clearTimeout(ct_hideTimeout); ct_hideTimeout = null; if (ct_copyPopupElement.style.visibility === 'hidden' || ct_copyPopupElement.style.opacity === '0') { ct_showCopyPopup(tagEl); } } else { clearTimeout(ct_showTimeout); clearTimeout(ct_hideTimeout); ct_hideTimeout = null; if (ct_currentHoveredTag && ct_copyPopupElement.style.visibility !== 'hidden' && ct_copyPopupElement.style.opacity !== '0') { ct_hideCopyPopupImmediately(false); } ct_currentHoveredTag = tagEl; ct_showTimeout = setTimeout(() => { if (ct_currentHoveredTag === tagEl && tagEl.matches(':hover')) { ct_showCopyPopup(tagEl); } ct_showTimeout = null; }, config.copyTag.hoverDelay); } } else if (isOverPopup) { clearTimeout(ct_hideTimeout); ct_hideTimeout = null; clearTimeout(ct_showTimeout); ct_showTimeout = null; if (ct_copyPopupElement.style.visibility === 'hidden' || ct_copyPopupElement.style.opacity === '0') { scheduleStyleUpdate(ct_copyPopupElement, { opacity: '1', visibility: 'visible', display: 'block' }); } } }
    function tp_handleMouseUp(event) { const target = event.target; if (target instanceof Node) { const isClickOnActionButton = popupElement?.contains(target) || tp_triggerButtonRef.element?.contains(target) || tp_cutButtonRef.element?.contains(target); const isClickOnToggle = togglePanelElement?.contains(target) || toggleTriggerElement?.contains(target); if (isClickOnActionButton || isClickOnToggle) return; } setTimeout(() => { requestAnimationFrame(() => { const latestSel = window.getSelection(); if (!latestSel) return; const hasStoredContent = !!(tp_storedHTML || tp_storedText); const targetEl = event.target instanceof Node ? event.target : null; const targetEditable = tp_isElementEditable(targetEl); if (latestSel.isCollapsed && hasStoredContent && targetEditable) { if (!popupElement || !popupElement.contains(event.target)) { console.log('[Mubu Helper TP] Showing paste button.'); hideSelectionActionButtons(); tp_showPasteButton(event); } } else { tp_hidePasteButton(); } }); }, config.transferPaste.buttonsAppearDelay); }
    function tp_handleKeyDown(event) { if (togglePanelElement?.contains(document.activeElement)) return; const anyActionEnabled = isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut') || isFeatureEnabled('selectSearchPopup'); if (!anyActionEnabled && !tp_pasteButtonRef.element) return; if ((event.ctrlKey || event.metaKey) && (event.key === 'a' || event.key === 'A')) { tp_ctrlApressed = true; hideSelectionActionButtons(); tp_hidePasteButton(); } else { if (tp_ctrlApressed && !(event.key === 'Control' || event.key === 'Meta' || event.key === 'Shift' || event.key === 'Alt')) { tp_ctrlApressed = false; } if (tp_pasteButtonRef.element?.style.visibility !== 'hidden' && !tp_pasteButtonRef.element.contains(event.target)) { if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Backspace', 'Delete', 'Enter'].includes(event.key)) { tp_hidePasteButton(); } } const actionButtonsVisible = (popupElement?.style.visibility !== 'hidden') || (tp_triggerButtonRef.element?.style.visibility !== 'hidden') || (tp_cutButtonRef.element?.style.visibility !== 'hidden'); const targetOnActionButtons = popupElement?.contains(event.target) || tp_triggerButtonRef.element?.contains(event.target) || tp_cutButtonRef.element?.contains(event.target); if (actionButtonsVisible && !targetOnActionButtons) { if (['Backspace', 'Delete'].includes(event.key)) { setTimeout(() => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) { hideSelectionActionButtons(); } }, 0); } } } }
    function tp_handleKeyUp(event) { if (togglePanelElement?.contains(document.activeElement)) return; const anyActionEnabled = isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut'); if (!anyActionEnabled) return; if (tp_ctrlApressed && (event.key === 'Control' || event.key === 'Meta' || event.key === 'a' || event.key === 'A')) { setTimeout(() => { const modPressed = event.ctrlKey || event.metaKey; if ((event.key === 'Control' || event.key === 'Meta') || ((event.key === 'a' || event.key === 'A') && !modPressed)) { if (tp_ctrlApressed) { const currentSelection = window.getSelection(); if (currentSelection && !currentSelection.isCollapsed && currentSelection.toString().trim().length > 0) { const range = currentSelection.rangeCount > 0 ? currentSelection.getRangeAt(0) : null; if (range) { const containerNode = range.commonAncestorContainer; const isInEditable = containerNode && ((containerNode.nodeType === Node.ELEMENT_NODE && tp_isElementEditable(containerNode)) || (containerNode.nodeType === Node.TEXT_NODE && containerNode.parentElement && tp_isElementEditable(containerNode.parentElement))); if (isInEditable) { requestAnimationFrame(() => { console.log('[Mubu Helper] Showing btns after Ctrl+A keyup'); showSelectionActionButtons(currentSelection, true); }); } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } tp_ctrlApressed = false; } } }, 0); } }
    function tp_initialize() { console.log('[Mubu TP] Initializing...'); const tpConfig = config.transferPaste; const MAX_RETRIES = tpConfig.initWaitMaxRetries; const RETRY_INTERVAL = tpConfig.initWaitRetryInterval; let retries = 0; const intervalId = setInterval(() => { if (tp_editorContainer) { clearInterval(intervalId); return; } const container = document.querySelector(tpConfig.editorContainerSelector); if (container) { clearInterval(intervalId); tp_editorContainer = container; console.log('[Mubu TP] Editor container found:', tp_editorContainer); tp_attachListeners(); } else { retries++; if (retries >= MAX_RETRIES) { clearInterval(intervalId); console.error(`[Mubu TP] Init failed: Container not found after ${MAX_RETRIES} retries.`); } } }, RETRY_INTERVAL); }
    function tp_attachListeners() { if (!tp_editorContainer) { console.warn('[Mubu TP] Attach skipped: Container not available.'); return; } if (tp_listenersAttached) { return; } console.log('[Mubu TP] Attaching listeners to:', tp_editorContainer); try { tp_editorContainer.addEventListener('keydown', tp_handleKeyDown, true); tp_editorContainer.addEventListener('keyup', tp_handleKeyUp, true); tp_listenersAttached = true; console.log('[Mubu TP] Key listeners attached.'); } catch (e) { console.error('[Mubu TP] Error attaching key listeners:', e); tp_listenersAttached = false; } }
    function tp_detachListeners() { if (!tp_editorContainer || !tp_listenersAttached) return; console.log('[Mubu TP] Detaching listeners from:', tp_editorContainer); try { tp_editorContainer.removeEventListener('keydown', tp_handleKeyDown, true); tp_editorContainer.removeEventListener('keyup', tp_handleKeyUp, true); tp_listenersAttached = false; console.log('[Mubu TP] Key listeners detached.'); } catch (e) { console.warn('[Mubu TP] Detach listeners err:', e); } }
    function hideTogglePanel() { if (togglePanelElement) { scheduleStyleUpdate(togglePanelElement, { transform: `translateX(100%)` }); } }
    function scheduleHideTogglePanel() { clearTimeout(togglePanelHideTimeout); togglePanelHideTimeout = setTimeout(() => { const triggerHover = toggleTriggerElement?.matches(':hover'); const panelHover = togglePanelElement?.matches(':hover'); if (!triggerHover && !panelHover) { hideTogglePanel(); } }, config.togglePanel.hideDelay); }
    function showTogglePanel() { clearTimeout(togglePanelHideTimeout); if (togglePanelElement) { scheduleStyleUpdate(togglePanelElement, { transform: 'translateX(0)' }); } }
    function createTogglePanel() { const panelId = config.togglePanel.panelId; const triggerId = config.togglePanel.triggerId; if (document.getElementById(panelId)) return; try { toggleTriggerElement = document.createElement('div'); toggleTriggerElement.id = triggerId; toggleTriggerElement.addEventListener('mouseenter', showTogglePanel); toggleTriggerElement.addEventListener('mouseleave', scheduleHideTogglePanel); document.body.appendChild(toggleTriggerElement); togglePanelElement = document.createElement('div'); togglePanelElement.id = panelId; togglePanelElement.innerHTML = '<div class="toggle-panel-title">功能开关</div>'; togglePanelElement.addEventListener('mouseenter', showTogglePanel); togglePanelElement.addEventListener('mouseleave', scheduleHideTogglePanel); for (const key in FEATURES) { if (FEATURES.hasOwnProperty(key)) { const feature = FEATURES[key]; const isEnabled = runtimeFeatureState[key]; const div = document.createElement('div'); div.className = 'toggle-control'; const label = document.createElement('label'); label.htmlFor = `toggle-${key}`; label.textContent = feature.label; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `toggle-${key}`; checkbox.checked = isEnabled; checkbox.dataset.featureKey = key; checkbox.addEventListener('change', (event) => { const changedKey = event.target.dataset.featureKey; const newState = event.target.checked; runtimeFeatureState[changedKey] = newState; console.log(`[Mubu] ${FEATURES[changedKey].label} ${newState ? '启用' : '禁用'}`); applyFeatureStateChange(changedKey, newState); }); div.appendChild(checkbox); div.appendChild(label); togglePanelElement.appendChild(div); } } document.body.appendChild(togglePanelElement); } catch (e) { console.error('[Mubu] Create toggle panel err:', e); togglePanelElement = null; toggleTriggerElement = null; } }
    let customInputListenerAttached = false; let historyListClickListenerAttached = false; let selectPopupListenersAttached = false; let copyTagListenerAttached = false;
    function applyFeatureStateChange(featureKey, isEnabled) {
        switch (featureKey) {
            case 'syncSearchBox':
                if (isEnabled) {
                    if (!topBarControls.container) createControlPanel();
                    if (topBarControls.container) scheduleStyleUpdate(topBarControls.container, { display: 'flex' });
                    if (!customInput && topBarControls.input) customInput = topBarControls.input;
                    if(topBarControls.input && !customInputListenerAttached) {
                        topBarControls.prevBtn?.addEventListener('click', historyNavPrevListener);
                        topBarControls.nextBtn?.addEventListener('click', historyNavNextListener);
                        topBarControls.clearBtn?.addEventListener('click', clearBtnListener);
                        topBarControls.input?.addEventListener('input', customInputListener, { passive: true });
                        customInputListenerAttached = true;
                    }
                    setupInputListeners(originalInput);
                    findAndSetupDebounced(); // Ensure observer finds input if it wasn't found initially
                } else {
                    if (topBarControls.container) scheduleStyleUpdate(topBarControls.container, { display: 'none' });
                    if(customInputListenerAttached) {
                        topBarControls.prevBtn?.removeEventListener('click', historyNavPrevListener);
                        topBarControls.nextBtn?.removeEventListener('click', historyNavNextListener);
                        topBarControls.clearBtn?.removeEventListener('click', clearBtnListener);
                        topBarControls.input?.removeEventListener('input', customInputListener);
                        customInputListenerAttached = false;
                    }
                    customInput = null;
                    // Still need to setup listeners if historyPanel is on but sync is off
                    setupInputListeners(originalInput);
                }
                break;
            case 'historyPanel':
                if (isEnabled) {
                    if (!historyPanel) createHistoryPanel();
                    if (historyPanel) scheduleStyleUpdate(historyPanel, { display: 'flex' });
                    if(historyListElement && !historyListClickListenerAttached) {
                        historyListElement.addEventListener('click', handleHistoryListClick);
                        historyListClickListenerAttached = true;
                    }
                    historyManager.updatePanel(); // Update panel on enable
                    setupInputListeners(originalInput); // Ensure correct listener is attached
                } else {
                    if (historyPanel) scheduleStyleUpdate(historyPanel, { display: 'none' });
                    if(historyListClickListenerAttached) {
                        historyListElement?.removeEventListener('click', handleHistoryListClick);
                        historyListClickListenerAttached = false;
                    }
                    // Need to potentially switch back to sync listener if sync is still on
                    setupInputListeners(originalInput);
                }
                break;
            // --- [ ☆ 推开内容逻辑 ☆ ] --- // <-- 新增逻辑处理
            case 'pushContent':
                try {
                    const pcConfig = config.pushContent;
                    const contentElement = document.querySelector(pcConfig.contentSelector);
                    if (contentElement) {
                        if (isEnabled) {
                            contentElement.classList.add(pcConfig.pushClass);
                            console.log(`[Mubu Push] Added class '${pcConfig.pushClass}' to ${pcConfig.contentSelector}`);
                        } else {
                            contentElement.classList.remove(pcConfig.pushClass);
                            console.log(`[Mubu Push] Removed class '${pcConfig.pushClass}' from ${pcConfig.contentSelector}`);
                        }
                    } else {
                        console.warn(`[Mubu Push] Target element "${pcConfig.contentSelector}" not found.`);
                    }
                } catch (e) {
                    console.error('[Mubu Push] Error applying state change:', e);
                }
                break;
            // --- [ ☆ 推开内容逻辑结束 ☆ ] ---
            case 'selectSearchPopup':
            case 'transferPasteCopy':
            case 'transferPasteCut':
                const anyGroupButtonEnabled = isFeatureEnabled('selectSearchPopup') || isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut');
                const anyPasteEnabled = isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut');

                if (anyGroupButtonEnabled) {
                    if (isFeatureEnabled('selectSearchPopup') && !popupElement) {
                        createSelectPopup();
                    }
                    if (!selectPopupListenersAttached) {
                        try {
                            document.addEventListener('mousedown', handleMouseDownPopup, true);
                            document.addEventListener('mouseup', handleMouseUpSelectionEnd, true);
                            // Attach paste button show logic listener only once if any related feature is on
                            document.addEventListener('mouseup', tp_handleMouseUp, true);
                            selectPopupListenersAttached = true;
                            console.log('[Mubu Helper] Global selection listeners attached.');
                        } catch (e) {
                             console.error('[Mubu Helper] Attach global listeners err:', e);
                        }
                    }
                } else {
                     if (selectPopupListenersAttached) {
                        try {
                            document.removeEventListener('mousedown', handleMouseDownPopup, true);
                            document.removeEventListener('mouseup', handleMouseUpSelectionEnd, true);
                            document.removeEventListener('mouseup', tp_handleMouseUp, true);
                            selectPopupListenersAttached = false;
                            console.log('[Mubu Helper] Global selection listeners detached.');
                        } catch(e) {
                             console.error('[Mubu Helper] Detach global listeners err:', e);
                        }
                    }
                    hideSelectionActionButtons(); // Hide buttons if all related features are off
                }

                // Attach/detach editor-specific listeners based on paste features
                if (anyPasteEnabled) {
                    tp_attachListeners();
                } else {
                    tp_detachListeners();
                    tp_hidePasteButton(); // Hide paste button if copy/cut are disabled
                }

                // Hide specific buttons if their feature was the one just disabled
                if (!isEnabled) {
                    if (featureKey === 'selectSearchPopup' && popupElement) hideSelectionActionButtons(); // Re-call to hide specifically filter button if needed
                    if (featureKey === 'transferPasteCopy' && tp_triggerButtonRef.element) hideSelectionActionButtons(); // Re-call to hide copy
                    if (featureKey === 'transferPasteCut' && tp_cutButtonRef.element) hideSelectionActionButtons(); // Re-call to hide cut
                }
                break;
            case 'copyTagOnHover':
                 if (isEnabled) {
                     ct_createElements(); // Ensure elements exist
                     if (!copyTagListenerAttached) {
                        // Try to find a more specific container, fall back to body
                        const target = document.querySelector(config.selectors.copyTagParentContainer) || document.body;
                        if (target) {
                           try {
                                target.addEventListener('mouseover', ct_handleMouseOver, { passive: true });
                                ct_listenerTarget = target;
                                copyTagListenerAttached = true;
                                console.log('[Mubu CT] Listener attached.');
                           } catch (e) {
                                console.error('[Mubu CT] Attach err:', e);
                                ct_listenerTarget = null; // Reset if attach failed
                           }
                        } else {
                             console.warn(`[Mubu CT] Target "${config.selectors.copyTagParentContainer}" not found for listener.`);
                        }
                     }
                 } else {
                    ct_hideCopyPopupImmediately(true); // Hide any visible popup
                    ct_hideFeedbackIndicator(); // Hide any feedback
                    if (copyTagListenerAttached && ct_listenerTarget) {
                        try {
                            ct_listenerTarget.removeEventListener('mouseover', ct_handleMouseOver);
                            copyTagListenerAttached = false;
                            ct_listenerTarget = null;
                            console.log('[Mubu CT] Listener detached.');
                        } catch(e) {
                            console.error('[Mubu CT] Detach err:', e);
                        }
                    } else {
                        // Ensure state is reset even if target wasn't found or attach failed
                        if(copyTagListenerAttached) copyTagListenerAttached = false;
                        ct_listenerTarget = null;
                    }
                    // Clear state variables
                    ct_currentHoveredTag = null;
                    ct_currentTagText = '';
                    clearTimeout(ct_showTimeout); ct_showTimeout = null;
                    clearTimeout(ct_hideTimeout); ct_hideTimeout = null;
                    clearTimeout(ct_feedbackTimeout); ct_feedbackTimeout = null;
                 }
                 break;
        }
    }

    function init() {
        console.log('[Mubu Combined Helper v3.7.0] 开始初始化...'); // Updated log message
        try {
            let featuresEnabledCount = 0;
            // Check prerequisites
            if ((FEATURES.syncSearchBox.enabled || FEATURES.selectSearchPopup.enabled || FEATURES.historyPanel.enabled) && !nativeInputValueSetter) {
                console.error("[Mubu] Native input value setter not found, critical for search features!");
            }
            if (FEATURES.copyTagOnHover.enabled && (!navigator.clipboard?.writeText)) {
                console.warn("[Mubu] Clipboard API (writeText) not available, 'Copy Tag' feature may fail.");
            }

            // --- [ ☆ CSS 注入 ☆ ] ---
            let combinedCSS = "";

            // Sync Search Box & History Panel CSS
            combinedCSS += `#${config.sync.topBarId}{position:fixed;top:1px;left:50%;transform:translateX(-50%);z-index:10001;background:rgba(255,255,255,0.98);padding:6px;border-radius:8px;box-shadow:0 2px 12px rgba(0,0,0,0.15);display:flex;gap:8px;align-items:center;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)} #${config.sync.topBarId} .custom-search-input{padding:8px 12px;border:1px solid #dcdfe6;border-radius:20px;width:300px;font-size:14px;transition:all .2s ease-in-out;background:#f8f9fa;color:#303133;box-sizing:border-box}#${config.sync.topBarId} .custom-search-input::-webkit-search-cancel-button,#${config.sync.topBarId} .custom-search-input::-webkit-search-clear-button{display:none;-webkit-appearance:none;appearance:none}#${config.sync.topBarId} .custom-search-input:focus{border-color:#5856d5;outline:0;background:#fff;box-shadow:0 0 0 1px #5856d5}#${config.sync.topBarId} .history-btn,#${config.sync.topBarId} .clear-btn{padding:6px 12px;background:#f0f2f5;border:1px solid #dcdfe6;border-radius:20px;cursor:pointer;transition:all .2s ease-in-out;font-weight:500;color:#606266;flex-shrink:0;user-select:none;line-height:1}#${config.sync.topBarId} .clear-btn{font-weight:bold;padding:6px 10px}#${config.sync.topBarId} .history-btn:hover,#${config.sync.topBarId} .clear-btn:hover{background:#e9e9eb;color:#5856d5;border-color:#c0c4cc}#${config.sync.topBarId} .history-btn:active,#${config.sync.topBarId} .clear-btn:active{transform:scale(.95);background:#dcdfe6}`;
            combinedCSS += ` #${config.sync.historyPanelId}{position:fixed;top:238px;right:0px;transform:translateY(-50%);z-index:10000;width:125px;max-height:380px;background:rgba(248,249,250,0.95);border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 8px rgba(0,0,0,0.1);padding:8px 0;overflow:hidden;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:flex;flex-direction:column}#${config.sync.historyPanelId} .search-history-list{list-style:none;padding:0;margin:0;overflow-y:auto;flex-grow:1;scrollbar-width:thin;scrollbar-color:#ccc #f0f0f0}#${config.sync.historyPanelId} .search-history-list::-webkit-scrollbar{width:6px}#${config.sync.historyPanelId} .search-history-list::-webkit-scrollbar-track{background:#f0f0f0;border-radius:3px}#${config.sync.historyPanelId} .search-history-list::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px}#${config.sync.historyPanelId} .search-history-item{padding:6px 12px;font-size:13px;color:#555;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;border-bottom:1px solid #eee;transition:background-color 0.1s ease-in-out,color 0.1s ease-in-out}#${config.sync.historyPanelId} .search-history-item:last-child{border-bottom:none}#${config.sync.historyPanelId} .search-history-item:hover{background-color:#e9e9eb;color:#5856d5}#${config.sync.historyPanelId} .search-history-item:active{background-color:#dcdfe6}#${config.sync.historyPanelId} .search-history-item--active{background-color:${config.sync.activeItemBgColor} !important;color:#000 !important;font-weight:500;}`;

            // Select Search Popup CSS
            combinedCSS += ` .mu-select-popup-btn { background-color:#5856d5;color:white;border:none;border-radius:5px;padding:4px 8px;font-size:14px;line-height:1;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,0.3);white-space:nowrap;user-select:none;-webkit-user-select:none; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out, background-color 0.1s ease-in-out; } .mu-select-popup-btn:hover { background-color:#4a48b3; }`;

            // Copy Tag CSS
            combinedCSS += ` #${config.copyTag.popupId}{position:absolute;top:0;left:0;z-index:10010;background-color:#f0f2f5;color:#5856d5;border:1px solid #dcdfe6;border-radius:4px;padding:2px 5px;font-size:12px;line-height:1;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.15);white-space:nowrap;user-select:none;-webkit-user-select:none;pointer-events:auto; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out;}#${config.copyTag.popupId}:hover{background-color:#e4e7ed;border-color:#c0c4cc}#${config.copyTag.feedbackId}{position:absolute;top:0;left:0;z-index:10011;background-color:#5856d5;color:white;border:1px solid #5856d5;border-radius:4px;padding:2px 5px;font-size:12px;line-height:1;cursor:default;box-shadow:0 1px 3px rgba(0,0,0,0.15);white-space:nowrap;user-select:none;-webkit-user-select:none;pointer-events:none; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out;}`;

            // Transfer Paste Buttons CSS
            const tpCss = config.transferPaste; const tpPref = tpCss.cssPrefix;
            combinedCSS += ` .${tpPref}${tpCss.btnBaseClass} { color:white;border:none;border-radius:5px;padding:4px 8px;font-size:14px;line-height:1;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,0.3);white-space:nowrap;user-select:none;-webkit-user-select:none; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out, background-color 0.1s ease-in-out; }`;
            combinedCSS += ` .${tpPref}${tpCss.btnCopyClass} { background-color:#5856d5; } .${tpPref}${tpCss.btnCopyClass}:hover { background-color:#4a48b3; }`;
            combinedCSS += ` .${tpPref}${tpCss.btnCutClass} { background-color:#d55856; } .${tpPref}${tpCss.btnCutClass}:hover { background-color:#b34a48; }`;
            combinedCSS += ` .${tpPref}${tpCss.btnPasteClass} { background-color:#5856d5; } .${tpPref}${tpCss.btnPasteClass}:hover { background-color:#4a48b3; }`;

            // Toggle Panel CSS
            const panelConf = config.togglePanel;
            combinedCSS += ` #${panelConf.triggerId}{position:fixed;bottom:0;right:0;width:${panelConf.triggerWidth}px;height:${panelConf.triggerHeight}px;background:rgba(0,0,0,0.01);cursor:pointer;z-index:19998;border-top-left-radius:5px;} #${panelConf.panelId}{position:fixed;bottom:0;right:0;width:${panelConf.panelWidth}px;max-height:80vh;overflow-y:auto;background:rgba(250,250,250,0.98);border:1px solid #ccc;border-top-left-radius:8px;box-shadow:-2px -2px 10px rgba(0,0,0,0.15);padding:10px;z-index:19999;transform:translateX(100%);transition:transform 0.3s ease-in-out;font-size:14px;color:#333;box-sizing:border-box;scrollbar-width:thin;scrollbar-color:#bbb #eee;} #${panelConf.panelId}::-webkit-scrollbar{width:6px;} #${panelConf.panelId}::-webkit-scrollbar-track{background:#eee;border-radius:3px;} #${panelConf.panelId}::-webkit-scrollbar-thumb{background-color:#bbb;border-radius:3px;} #${panelConf.triggerId}:hover + #${panelConf.panelId}, #${panelConf.panelId}:hover{transform:translateX(0);} #${panelConf.panelId} .toggle-panel-title{font-weight:bold;margin-bottom:10px;padding-bottom:5px;border-bottom:1px solid #eee;text-align:center;} #${panelConf.panelId} .toggle-control{display:flex;align-items:center;margin-bottom:8px;cursor:pointer;} #${panelConf.panelId} .toggle-control input[type="checkbox"]{margin-right:8px;cursor:pointer;appearance:none;-webkit-appearance:none;width:36px;height:20px;background-color:#ccc;border-radius:10px;position:relative;transition:background-color 0.2s ease-in-out;flex-shrink:0;} #${panelConf.panelId} .toggle-control input[type="checkbox"]::before{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background-color:white;top:2px;left:2px;transition:left 0.2s ease-in-out;box-shadow:0 1px 2px rgba(0,0,0,0.2);} #${panelConf.panelId} .toggle-control input[type="checkbox"]:checked{background-color:#5856d5;} #${panelConf.panelId} .toggle-control input[type="checkbox"]:checked::before{left:18px;} #${panelConf.panelId} .toggle-control label{flex-grow:1;user-select:none;cursor:pointer;}`;

            // --- [ ☆ 推开内容 CSS ☆ ] --- // <-- 新增 CSS 注入
            const pcConf = config.pushContent;
            combinedCSS += `
                ${pcConf.contentSelector} {
                    transition: margin-right ${pcConf.transitionDuration} ease-in-out !important;
                    box-sizing: border-box; /* Good practice */
                }
                ${pcConf.contentSelector}.${pcConf.pushClass} {
                    margin-right: ${pcConf.pushMarginRight}px !important;
                }
            `;
            // --- [ ☆ 推开内容 CSS 结束 ☆ ] ---

            // Apply all CSS
            if (combinedCSS) {
                try { GM_addStyle(combinedCSS); } catch (e) { console.warn('[Mubu] Inject CSS err:', e); }
            }

            // Create UI elements
            createControlPanel();
            createHistoryPanel();
            createTogglePanel(); // Creates the panel and checkboxes

            // Initialize subsystems
            tp_initialize(); // Initialize transfer paste (finds editor, attaches key listeners later if needed)

            // Setup initial input state and listeners if needed
            const needsInputLogic = isFeatureEnabled('syncSearchBox') || isFeatureEnabled('historyPanel') || isFeatureEnabled('selectSearchPopup');
            if (needsInputLogic) {
                const initialInput = optimizedFindSearchBox();
                if (initialInput) {
                    originalInput = initialInput;
                    setupInputListeners(originalInput); // Attaches appropriate listener based on enabled features
                    updateCustomInputAndAddHistory(initialInput.value, 'init'); // Initial sync/history add
                } else {
                    console.warn("[Mubu] Initial search input not found on load.");
                }
            }

            // Setup DOM observer for search input changes if needed
            const needsObserver = isFeatureEnabled('syncSearchBox') || (isFeatureEnabled('historyPanel') && !isFeatureEnabled('syncSearchBox') && isHistoryTrackingNeeded());
            if (needsObserver) {
                const target = document.querySelector(config.selectors.domObserverTarget) || document.body;
                if (target) {
                    domObserver = new MutationObserver((mutations) => {
                        // Check if observer should still be active
                        if (!isFeatureEnabled('syncSearchBox') && !isFeatureEnabled('historyPanel')) {
                            domObserver.disconnect(); // Disconnect if no longer needed
                            domObserver = null;
                            console.log('[Mubu] DOM Observer disconnected as features are off.');
                            return;
                        }
                        let relevant = mutations.some(m => {
                             if (m.type === 'childList') return true; // Input added/removed
                             // Check if the specific input element became disabled/enabled or was removed
                             if (m.type === 'attributes' && m.target === originalInput && m.attributeName === 'disabled') return true;
                             if (m.type === 'childList' && m.removedNodes.length > 0) {
                                 for (const node of m.removedNodes) {
                                      if (node === originalInput || (node instanceof Element && node.contains?.(originalInput))) {
                                           return true; // Input or its container removed
                                      }
                                 }
                             }
                             return false;
                        });
                        if(relevant) findAndSetupDebounced(); // Debounced check and setup
                    });
                    domObserver.observe(target, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled'] });
                    console.log('[Mubu] DOM Observer attached.');
                } else {
                    console.error(`[Mubu] Observer target "${config.selectors.domObserverTarget}" not found! Search input might not be detected dynamically.`);
                }
            }

            // Apply initial state for all features based on FEATURES defaults / runtime state
            console.log('[Mubu Helper] Applying initial feature states...');
            let initialEnabledCount = 0;
            for (const key in runtimeFeatureState) {
                if (runtimeFeatureState[key]) { // If the feature is enabled by default or runtime state
                    console.log(`[Mubu Helper] Initializing enabled feature: ${key}`);
                    try {
                        applyFeatureStateChange(key, true); // Apply the 'enabled' state
                        initialEnabledCount++;
                    } catch (applyError) {
                        console.error(`[Mubu Helper] Error applying initial state for ${key}:`, applyError);
                    }
                }
            }
            featuresEnabledCount = initialEnabledCount;
            console.log('[Mubu Helper] Initial feature states applied.');


            // Add unload listener for cleanup
            if (featuresEnabledCount > 0 || togglePanelElement) { // Add cleanup if any feature was on or panel exists
               window.addEventListener('unload', cleanup);
               console.log(`[Mubu Combined Helper v3.7.0] Init complete (${featuresEnabledCount} features initially active, panel loaded)`);
            } else {
               window.addEventListener('unload', cleanup); // Still add cleanup for panel removal etc.
               console.log('[Mubu Combined Helper v3.7.0] Init complete (No features initially active, panel loaded)');
            }

        } catch (initError) {
            console.error('[Mubu Combined Helper v3.7.0] FATAL INIT ERROR:', initError);
        }
    }

    function cleanup() {
        console.log('[Mubu Combined Helper v3.7.0] Cleaning up...'); // Updated log message
        window.removeEventListener('unload', cleanup);

        try {
            // Disconnect observers
            domObserver?.disconnect();
            observerInstance.disconnect();
            elementObserverMap.clear();
            domObserver = null;

            // Remove listeners
            teardownInputListeners(originalInput); // Removes input listeners
            if (customInputListenerAttached) { // Remove custom input listeners
                try {
                    topBarControls.prevBtn?.removeEventListener('click', historyNavPrevListener);
                    topBarControls.nextBtn?.removeEventListener('click', historyNavNextListener);
                    topBarControls.clearBtn?.removeEventListener('click', clearBtnListener);
                    topBarControls.input?.removeEventListener('input', customInputListener);
                } catch(e){}
                customInputListenerAttached = false;
            }
            if (historyListClickListenerAttached) { // Remove history list listener
                try { historyListElement?.removeEventListener('click', handleHistoryListClick); } catch(e){}
                historyListClickListenerAttached = false;
            }
            if (selectPopupListenersAttached) { // Remove global selection listeners
                try {
                    document.removeEventListener('mousedown', handleMouseDownPopup, true);
                    document.removeEventListener('mouseup', handleMouseUpSelectionEnd, true);
                    document.removeEventListener('mouseup', tp_handleMouseUp, true);
                } catch(e){}
                 selectPopupListenersAttached = false;
            }
            if (copyTagListenerAttached && ct_listenerTarget) { // Remove copy tag listener
                 try { ct_listenerTarget.removeEventListener('mouseover', ct_handleMouseOver); } catch(e){}
                 copyTagListenerAttached = false;
                 ct_listenerTarget = null;
            }
            tp_detachListeners(); // Detach editor key listeners

            // Clear timeouts
            clearTimeout(togglePanelHideTimeout);
            clearTimeout(ct_showTimeout);
            clearTimeout(ct_hideTimeout);
            clearTimeout(ct_feedbackTimeout);

            // Remove toggle panel listeners
            toggleTriggerElement?.removeEventListener('mouseenter', showTogglePanel);
            toggleTriggerElement?.removeEventListener('mouseleave', scheduleHideTogglePanel);
            togglePanelElement?.removeEventListener('mouseenter', showTogglePanel);
            togglePanelElement?.removeEventListener('mouseleave', scheduleHideTogglePanel);

            // Hide dynamic UI elements
            hideSelectionActionButtons();
            tp_hidePasteButton();
            ct_hideCopyPopupImmediately(true);
            ct_hideFeedbackIndicator();
            hideTogglePanel(); // Hide the panel itself

             // --- [ ☆ 推开内容清理 ☆ ] --- // <-- 新增清理
            try {
                const pcConfig = config.pushContent;
                const contentElement = document.querySelector(pcConfig.contentSelector);
                contentElement?.classList.remove(pcConfig.pushClass); // Ensure class is removed
            } catch(e) {
                console.warn('[Mubu Push] Error during cleanup:', e);
            }
            // --- [ ☆ 推开内容清理结束 ☆ ] ---


            // Schedule removal of dynamically added elements
            // Use setTimeout to avoid issues if cleanup happens during element interaction
            setTimeout(() => {
                try { popupElement?.remove(); } catch(e){}
                try { tp_triggerButtonRef.element?.remove(); } catch(e){}
                try { tp_cutButtonRef.element?.remove(); } catch(e){}
                try { tp_pasteButtonRef.element?.remove(); } catch(e){}
                try { ct_copyPopupElement?.remove(); } catch(e){}
                try { ct_feedbackElement?.remove(); } catch(e){}
            }, 200);

            // Remove static UI elements added by the script
            try { topBarControls.container?.remove(); } catch(e){}
            try { historyPanel?.remove(); } catch(e){}
            try { toggleTriggerElement?.remove(); } catch(e){}
            try { togglePanelElement?.remove(); } catch(e){}


            // Reset state variables
            isRafScheduled=false; styleUpdateQueue=[];
            originalInput=null; lastSyncedValue=null; isSyncing=false; customInput=null;
            originalInputSyncHandler=null; originalInputHistoryHandler=null;
            topBarControls={}; historyPanel=null; historyListElement=null; activeHistoryItemElement=null;
            popupElement=null; currentSelectedText='';
            ct_copyPopupElement=null; ct_feedbackElement=null; ct_currentHoveredTag=null; ct_currentTagText='';
            ct_showTimeout=null; ct_hideTimeout=null; ct_feedbackTimeout=null; ct_listenerTarget=null;
            tp_editorContainer=null; tp_triggerButtonRef.element=null; tp_cutButtonRef.element=null; tp_pasteButtonRef.element=null;
            tp_storedHTML=''; tp_storedText=''; tp_ctrlApressed=false; // tp_listenersAttached is reset in tp_detachListeners
            togglePanelElement=null; toggleTriggerElement=null; togglePanelHideTimeout=null;

        } catch (cleanupError) {
            console.warn('[Mubu] Cleanup error:', cleanupError);
        }
        console.log('[Mubu Combined Helper v3.7.0] Cleanup complete.'); // Updated log message
    }

    // --- Initialization Trigger ---
    // Wait for the document to be sufficiently loaded before initializing
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(init, config.initDelay);
    } else {
        window.addEventListener('DOMContentLoaded', () => setTimeout(init, config.initDelay), { once: true });
    }

})();