// ==UserScript==
// @name 幕布mubu搜索历史记录前进后退
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 1.1
// @author YourName
// @match https://mubu.com/*
// @grant GM_addStyle
// @license MIT
// ==/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();
})();