// ==UserScript== // @name 幕布mubu搜索历史记录前进后退 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 1.1 // @author YourName // @match https://mubu.com/* // @grant GM_addStyle // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; GM_addStyle(` .custom-search-container { position: fixed; top: 1px; left: 50%; transform: translateX(-50%); z-index: 9999; background: white; padding: 6px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: flex; gap: 8px; align-items: center; will-change: transform; } .custom-search-input { padding: 8px 12px; border: 1px solid #e0e0e0; border-radius: 20px; width: 300px; font-size: 14px; transition: border-color 0.3s, box-shadow 0.3s; background: #f8f8f8; } .custom-search-input:focus { border-color: #5856d5; outline: none; background: white; box-shadow: 0 0 8px rgba(64,158,255,0.2); } .history-btn { padding: 6px 12px; background: #f0f0f0; border: 1px solid #e0e0e0; border-radius: 20px; cursor: pointer; transition: all 0.2s; font-weight: 500; color: #666; } .history-btn:hover { background: #5856d5; color: white; border-color: #5856d5; transform: scale(1.05); } .history-btn:active { transform: scale(0.95); } `); const config = { historySize: 64, mask: 0x3F, debounceTime: 40, mutationDebounce: 150, cacheTTL: 2000, observerConfig: { valueObserver: { attributeFilter: ['value'], attributeOldValue: true, subtree: true }, domObserver: { childList: true, subtree: true, attributes: false, characterData: false } } }; const cache = { inputElement: null, lastCacheTime: 0, get valid() { return performance.now() - this.lastCacheTime < config.cacheTTL } }; const optimizedFindSearchBox = (() => { let observer; const selector = 'input[placeholder="搜索关键词"]:not([disabled])'; const updateCache = (target) => { if (!target || !target.matches(selector)) return; if (cache.inputElement === target) return; cache.inputElement = target; cache.lastCacheTime = performance.now(); }; const initObserver = () => { observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.addedNodes) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const found = node.matches(selector) ? node : node.querySelector(selector); if (found) updateCache(found); } } } } }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: false, characterData: false }); }; return { get: () => { if (!observer) initObserver(); if (!cache.valid || !cache.inputElement?.isConnected) { const freshElement = document.querySelector(selector); if (freshElement) updateCache(freshElement); } return cache.inputElement; }, disconnect: () => observer?.disconnect() }; })(); const historyManager = (() => { const buffer = new Array(config.historySize); let writePtr = 0; let count = 0; let precomputedIndexes = new Array(config.historySize); const updatePrecomputed = () => { for (let i = 0; i < count; i++) { precomputedIndexes[i] = (writePtr - count + i + config.historySize) & config.mask; } }; return { add: (value) => { const trimmed = String(value).trim(); if (!trimmed) return; const prevIndex = (writePtr - 1) & config.mask; if (trimmed === buffer[prevIndex]) return; buffer[writePtr] = trimmed; writePtr = (writePtr + 1) & config.mask; count = Math.min(count + 1, config.historySize); updatePrecomputed(); }, get: (index) => buffer[precomputedIndexes[index]] || '', size: () => count, currentIndex: -1 }; })(); const createSyncHandler = (() => { const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); const inputEvent = new Event('input', { bubbles: true }); let lastSync = 0; return { sync: (source, target) => { if (source.value === target.value) return; const now = performance.now(); if (now - lastSync < config.debounceTime) return; descriptor.set.call(target, source.value); target.dispatchEvent(inputEvent); lastSync = now; } }; })(); const throttle = (fn, delay) => { let lastExec = 0; let pendingFrame = null; const throttled = (...args) => { const now = performance.now(); const elapsed = now - lastExec; const execute = () => { fn(...args); lastExec = performance.now(); pendingFrame = null; }; if (elapsed > delay) { if (pendingFrame) { cancelAnimationFrame(pendingFrame); pendingFrame = null; } execute(); } else if (!pendingFrame) { pendingFrame = requestAnimationFrame(() => { if (performance.now() - lastExec >= delay) { execute(); } }); } }; throttled.cancel = () => { if (pendingFrame) cancelAnimationFrame(pendingFrame); }; return throttled; }; const createControlPanel = () => { const container = Object.assign(document.createElement('div'), { className: 'custom-search-container' }); const [prevBtn, nextBtn] = ['←', '→'].map(text => Object.assign(document.createElement('button'), { className: 'history-btn', textContent: text }) ); const input = Object.assign(document.createElement('input'), { className: 'custom-search-input', placeholder: '筛选' }); container.append(prevBtn, nextBtn, input); document.body.appendChild(container); return { input: input, prevBtn: prevBtn, nextBtn: nextBtn }; }; const initSystem = () => { const { input: customInput, prevBtn, nextBtn } = createControlPanel(); let lastValue = ''; let mutationTimeout = null; let valueObserver = null; let domObserver = null; let originalInput = null; let originalInputHandler = null; const setupValueObserver = (target) => { valueObserver?.disconnect(); valueObserver = new MutationObserver(() => { if (!target || isSyncing) return; const currentValue = target.value; if (currentValue !== lastValue) { isSyncing = true; customInput.value = lastValue = currentValue; historyManager.add(currentValue); historyManager.currentIndex = historyManager.size() - 1; isSyncing = false; } }); valueObserver.observe(target, config.observerConfig.valueObserver); }; const bindEvents = (target) => { const newHandler = () => { if (!isSyncing) { isSyncing = true; customInput.value = target.value; historyManager.add(target.value); historyManager.currentIndex = historyManager.size() - 1; isSyncing = false; } }; target.removeEventListener('input', originalInputHandler); target.addEventListener('input', newHandler); originalInputHandler = newHandler; customInput.addEventListener('input', () => createSyncHandler.sync(customInput, target), { passive: true } ); const navigateFactory = (direction) => throttle(() => { const newIndex = historyManager.currentIndex + direction; if (newIndex < -1 || newIndex >= historyManager.size()) return; historyManager.currentIndex = Math.max(-1, Math.min(newIndex, historyManager.size() - 1)); isSyncing = true; valueObserver?.disconnect(); customInput.value = historyManager.get(historyManager.currentIndex); createSyncHandler.sync(customInput, target); if (valueObserver && target) { valueObserver.observe(target, config.observerConfig.valueObserver); } isSyncing = false; }, 120); prevBtn.addEventListener('click', navigateFactory(-1)); nextBtn.addEventListener('click', navigateFactory(1)); }; domObserver = new MutationObserver(() => { clearTimeout(mutationTimeout); mutationTimeout = setTimeout(() => { const newInput = optimizedFindSearchBox.get(); if (newInput && newInput !== originalInput) { originalInput = newInput; lastValue = newInput.value; setupValueObserver(newInput); bindEvents(newInput); } }, config.mutationDebounce); }); const contentNode = document.querySelector('#root > div') || document.body; domObserver.observe(contentNode, config.observerConfig.domObserver); const initialInput = optimizedFindSearchBox.get(); if (initialInput) { originalInput = initialInput; lastValue = initialInput.value; setupValueObserver(initialInput); bindEvents(initialInput); } }; const initialize = () => { document.body ? initSystem() : window.addEventListener('DOMContentLoaded', initSystem); }; window.addEventListener('unload', () => { optimizedFindSearchBox.disconnect(); valueObserver?.disconnect(); domObserver?.disconnect(); originalInput?.removeEventListener('input', originalInputHandler); [valueObserver, domObserver, originalInput, originalInputHandler].forEach(item => item = null); }); let isSyncing = false; initialize(); })();