您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Draggable floating search with multi-term partial AND filtering and price re-sort; compact buttons, theme selector dropdown, added orange Dracula theme, grey theme gets a black border. Position & theme saved.
// ==UserScript== // @name Camerax Floating Live Multi-Term Search + Price Sort (Draggable, Themeable, Compact UI) // @namespace http://tampermonkey.net/ // @version 2.5 // @description Draggable floating search with multi-term partial AND filtering and price re-sort; compact buttons, theme selector dropdown, added orange Dracula theme, grey theme gets a black border. Position & theme saved. // @match https://camerax.com/product-category/* // @grant none // @license GNU GPLv3 // ==/UserScript== (function() { 'use strict'; /* ---------- CONFIG ---------- */ const FLOAT_WIDTH = 300; // px const DEBOUNCE_MS = 160; const STORAGE_KEY_POS = 'camx-floating-pos-v1'; const STORAGE_KEY_THEME = 'camx-floating-theme-v1'; /* ---------------------------- */ // Theme definitions const THEMES = { dracula: { name: 'Dracula', bg: '#282a36', inputBg: '#44475a', inputColor: '#f8f8f2', controlBg: '#ffffff', controlText: '#282a36', subtleBg: '#44475a', border: '#6272a4', smallText: '#f8f8f2', boxShadow: '0 6px 18px rgba(0,0,0,0.45)' }, dracula_orange: { name: 'Dracula (Orange)', bg: '#212025', inputBg: '#3b3a3f', inputColor: '#f8f8f2', controlBg: '#ffb86c', // orange accent for buttons controlText: '#2b2a2a', subtleBg: '#38343a', border: '#ffb86c', smallText: '#f8f8f2', boxShadow: '0 6px 20px rgba(0,0,0,0.55)' }, grey: { name: 'Grey', bg: '#efefef', inputBg: '#ffffff', inputColor: '#1a1a1a', controlBg: '#ffffff', controlText: '#333333', subtleBg: '#f5f5f5', border: '#000000', // changed to black border per request smallText: '#333333', boxShadow: '0 6px 18px rgba(30,30,30,0.08)' } }; function log(...args) { try { console.log('[CamX-userscript]', ...args); } catch (e) {} } function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } function findProductsContainer() { return document.querySelector('.products') || document.querySelector('.products.row') || document.querySelector('ul.products') || document.querySelector('#main .content') || document.querySelector('.shop-container') || null; } function collectProductEntries(container) { if (!container) return []; const productEls = Array.from(container.querySelectorAll('.product-small')); const entries = productEls.map(productEl => { let wrapper = productEl; try { while (wrapper.parentElement && wrapper.parentElement !== container && wrapper !== document.body) { wrapper = wrapper.parentElement; } } catch (e) {} if (!wrapper || wrapper.parentElement !== container) wrapper = productEl; return { productEl, wrapperEl: wrapper }; }); const seen = new Set(); const dedup = []; for (const e of entries) { if (!seen.has(e.wrapperEl)) { seen.add(e.wrapperEl); dedup.push(e); } } return dedup; } function parsePriceFromWrapper(el) { if (!el) return Infinity; try { const amounts = Array.from(el.querySelectorAll('.woocommerce-Price-amount')); if (amounts.length === 0) return Infinity; const node = amounts[amounts.length - 1]; const raw = (node.textContent || '').replace(/[^0-9.]/g, ''); const n = parseFloat(raw); return Number.isFinite(n) ? n : Infinity; } catch (e) { return Infinity; } } function getTitleAndMeta(entry) { try { const titleNode = entry.productEl.querySelector('.product-title a, .woocommerce-loop-product__title, .name.product-title'); const title = (titleNode && titleNode.textContent || '').trim(); const condNode = entry.productEl.querySelector('.condition-grade-shop, .condition-grade, .product-info .stock, .price-wrapper, .product-meta'); const condText = condNode ? condNode.textContent.trim() : ''; return (title + ' ' + condText).replace(/\s+/g, ' ').trim(); } catch (e) { return ''; } } function filterAndResort(container, desc = false, query = '') { if (!container) return; try { const entries = collectProductEntries(container); const tokens = (query || '').toLowerCase().trim().split(/\s+/).filter(t => t.length > 0); entries.forEach(entry => { const text = (getTitleAndMeta(entry) || '').toLowerCase(); let visible = true; for (const tok of tokens) { if (!text.includes(tok)) { visible = false; break; } } try { entry.wrapperEl.style.display = visible ? '' : 'none'; } catch (e) {} }); const visible = entries .filter(e => e.wrapperEl && e.wrapperEl.offsetParent !== null) .sort((a, b) => { const pa = parsePriceFromWrapper(a.wrapperEl); const pb = parsePriceFromWrapper(b.wrapperEl); return desc ? pb - pa : pa - pb; }); visible.forEach(e => { try { container.appendChild(e.wrapperEl); } catch (err) {} }); } catch (err) { log('filterAndResort error', err); } } // Apply theme styles to the UI elements function applyThemeToUI(uiElements, themeName) { const theme = THEMES[themeName] || THEMES.dracula; const { root, input, toggle, clear, themeSelect, title, small } = uiElements; root.style.background = theme.bg; root.style.boxShadow = theme.boxShadow; title.style.color = theme.smallText; small.style.color = theme.smallText; // Input input.style.background = theme.inputBg; input.style.color = theme.inputColor; input.style.border = `1px solid ${theme.border}`; // Toggle (primary) toggle.style.background = theme.controlBg; toggle.style.color = theme.controlText; toggle.style.border = `1px solid ${theme.border}`; // Clear clear.style.background = theme.controlBg; clear.style.color = theme.controlText; clear.style.border = `1px solid ${theme.border}`; // Theme selector if (themeSelect) { themeSelect.style.background = theme.controlBg; themeSelect.style.color = theme.controlText; themeSelect.style.border = `1px solid ${theme.border}`; } } // Create floating UI (draggable + compact buttons + theme select) function createFloatingUI(onChangeCallback) { // Avoid duplicate if (document.getElementById('camx-floating-filter')) { const existingInput = document.querySelector('#camx-floating-filter input[type="search"]'); return { root: document.getElementById('camx-floating-filter'), input: existingInput, getDesc: () => document.getElementById('camx-toggle')?.dataset.desc === '1' }; } const wrap = document.createElement('div'); wrap.id = 'camx-floating-filter'; wrap.style.position = 'fixed'; wrap.style.top = '18px'; wrap.style.left = ''; wrap.style.zIndex = '999999'; wrap.style.width = FLOAT_WIDTH + 'px'; wrap.style.padding = '8px'; wrap.style.borderRadius = '10px'; wrap.style.fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial'; wrap.style.color = '#fff'; wrap.style.display = 'flex'; wrap.style.flexDirection = 'column'; wrap.style.gap = '6px'; wrap.style.alignItems = 'stretch'; wrap.style.userSelect = 'none'; const header = document.createElement('div'); header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.gap = '6px'; header.style.cursor = 'move'; header.style.padding = '2px 4px'; const title = document.createElement('div'); title.textContent = 'Filter & Sort'; title.style.fontWeight = '700'; title.style.fontSize = '12px'; title.style.pointerEvents = 'none'; const small = document.createElement('div'); small.style.fontSize = '11px'; small.textContent = 'price'; small.style.opacity = '0.95'; small.style.pointerEvents = 'none'; header.appendChild(title); header.appendChild(small); wrap.appendChild(header); const input = document.createElement('input'); input.type = 'search'; input.placeholder = 'space-separated terms (AND). partial words OK'; input.style.padding = '8px'; input.style.fontSize = '13px'; input.style.borderRadius = '6px'; input.style.width = '100%'; input.autocomplete = 'off'; // controls row (compact) const controls = document.createElement('div'); controls.style.display = 'flex'; controls.style.gap = '6px'; controls.style.alignItems = 'center'; // Asc/Desc toggle (small) const toggle = document.createElement('button'); toggle.id = 'camx-toggle'; toggle.dataset.desc = '0'; toggle.textContent = 'Asc'; toggle.title = 'Toggle Asc/Desc'; toggle.style.flex = '1'; toggle.style.padding = '3px 6px'; toggle.style.fontSize = '11px'; toggle.style.borderRadius = '5px'; toggle.style.cursor = 'pointer'; // Clear (small) const clear = document.createElement('button'); clear.textContent = 'Clear'; clear.title = 'Clear filter'; clear.style.padding = '3px 6px'; clear.style.fontSize = '11px'; clear.style.borderRadius = '5px'; clear.style.cursor = 'pointer'; // Theme selector (dropdown) const themeSelect = document.createElement('select'); themeSelect.id = 'camx-theme-select'; themeSelect.style.padding = '3px 6px'; themeSelect.style.fontSize = '11px'; themeSelect.style.borderRadius = '5px'; themeSelect.style.cursor = 'pointer'; themeSelect.style.minWidth = '92px'; // Populate theme options const themeKeys = Object.keys(THEMES); for (const key of themeKeys) { const opt = document.createElement('option'); opt.value = key; opt.textContent = THEMES[key].name; themeSelect.appendChild(opt); } controls.appendChild(toggle); controls.appendChild(clear); controls.appendChild(themeSelect); wrap.appendChild(input); wrap.appendChild(controls); document.body.appendChild(wrap); // Position logic try { const saved = localStorage.getItem(STORAGE_KEY_POS); const rect = wrap.getBoundingClientRect(); if (saved) { const pos = JSON.parse(saved); if (typeof pos.left === 'number' && typeof pos.top === 'number') { wrap.style.left = Math.min(Math.max(0, pos.left), window.innerWidth - rect.width) + 'px'; wrap.style.top = Math.min(Math.max(0, pos.top), window.innerHeight - rect.height) + 'px'; } } else { const left = Math.max(6, window.innerWidth - rect.width - 18); wrap.style.left = left + 'px'; } } catch (e) { try { wrap.style.left = Math.max(6, window.innerWidth - FLOAT_WIDTH - 18) + 'px'; } catch (e2) {} } // Draggable (pointer events) (function makeDraggable(handle, element) { let dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0; function onPointerDown(e) { const tgt = e.target; if (tgt.closest('input, select, button')) return; dragging = true; element.setPointerCapture?.(e.pointerId); startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; element.style.transition = 'none'; e.preventDefault(); } function onPointerMove(e) { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; let nx = Math.round(startLeft + dx); let ny = Math.round(startTop + dy); const rect = element.getBoundingClientRect(); nx = Math.min(Math.max(0, nx), window.innerWidth - rect.width); ny = Math.min(Math.max(0, ny), window.innerHeight - rect.height); element.style.left = nx + 'px'; element.style.top = ny + 'px'; } function onPointerUp(e) { if (!dragging) return; dragging = false; try { element.releasePointerCapture?.(e.pointerId); } catch (e) {} const rect = element.getBoundingClientRect(); try { localStorage.setItem(STORAGE_KEY_POS, JSON.stringify({ left: rect.left, top: rect.top })); } catch (err) {} element.style.transition = ''; } handle.addEventListener('pointerdown', onPointerDown, { passive: false }); window.addEventListener('pointermove', onPointerMove); window.addEventListener('pointerup', onPointerUp); window.addEventListener('pointercancel', onPointerUp); })(header, wrap); // Theme handling function getSavedTheme() { try { const t = localStorage.getItem(STORAGE_KEY_THEME); return (t && THEMES[t]) ? t : 'dracula'; } catch (e) { return 'dracula'; } } let activeTheme = getSavedTheme(); themeSelect.value = activeTheme; applyThemeToUI({ root: wrap, input, toggle, clear, themeSelect, title, small }, activeTheme); themeSelect.addEventListener('change', () => { activeTheme = themeSelect.value; try { localStorage.setItem(STORAGE_KEY_THEME, activeTheme); } catch (e) {} applyThemeToUI({ root: wrap, input, toggle, clear, themeSelect, title, small }, activeTheme); }); // Wiring behavior let desc = false; toggle.addEventListener('click', () => { desc = !desc; toggle.dataset.desc = desc ? '1' : '0'; toggle.textContent = desc ? 'Desc' : 'Asc'; onChangeCallback(input.value, desc); }); clear.addEventListener('click', () => { input.value = ''; onChangeCallback('', desc); input.focus(); }); input.addEventListener('input', debounce(() => onChangeCallback(input.value, desc), DEBOUNCE_MS)); return { root: wrap, input, toggle, clear, themeSelect, getDesc: () => toggle.dataset.desc === '1' }; } function initWhenReady(retries = 50) { const container = findProductsContainer(); if (container) { initApp(container); return; } if (retries <= 0) { log('products container not found; giving up.'); return; } setTimeout(() => initWhenReady(retries - 1), 300); } function initApp(container) { try { const ui = createFloatingUI((query, desc) => { filterAndResort(container, desc, query); }); // initial run filterAndResort(container, ui.getDesc(), ui.input.value || ''); // observe container for dynamic changes const mo = new MutationObserver(debounce(() => { try { filterAndResort(container, ui.getDesc(), ui.input.value || ''); } catch (e) { log('MO callback error', e); } }, 300)); mo.observe(container, { childList: true, subtree: true }); window.addEventListener('popstate', () => { setTimeout(() => filterAndResort(container, ui.getDesc(), ui.input.value || ''), 300); }); log('CamX userscript initialized (draggable themeable compact)'); } catch (e) { log('initApp error', e); } } initWhenReady(); })();