// ==UserScript== // @name SL Marketplace Cleaner // @namespace slmarketplace // @version 0.72 // @description Clean up Second Life Marketplace search results // @match https://marketplace.secondlife.com/* // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function () { 'use strict'; const DEFAULT_SETTINGS = { blacklist: [], blacklistEnabled: true, negativeKeywords: [], negativeKeywordsEnabled: true, maxPerStore: -1, collapseColors: true, collapseBodies: true, preferredBody: 'Reborn', hideDemos: true, hideLimited: true, showCurrency: true, currency: 'EUR', usdPerLinden: 1 / 250, eurPerUsd: 0.87, layoutEnabled: true, theme: 'day', debug: false, }; const SETTING_KEYS = Object.keys(DEFAULT_SETTINGS); function loadSettings() { const s = {}; for (const key of SETTING_KEYS) { s[key] = GM_getValue(key, DEFAULT_SETTINGS[key]); } return s; } const settings = loadSettings(); function saveSetting(key, value) { settings[key] = value; GM_setValue(key, value); } function coerceSetting(key, value) { const def = DEFAULT_SETTINGS[key]; if (typeof def === 'boolean') return !!value; if (typeof def === 'number') return Number(value); return value; } function setSetting(key, value) { saveSetting(key, coerceSetting(key, value)); refreshAll(); syncUIFromSettings(); refreshAppearance(); notifySettingsChanged(); } const SETTINGS_SYNC_KEY = 'slmc-settings-version'; function notifySettingsChanged() { try { localStorage.setItem(SETTINGS_SYNC_KEY, String(Date.now())); } catch (e) { /* storage unavailable - no cross-tab sync */ } } function applyRemoteSettings() { Object.assign(settings, loadSettings()); refreshAll(); syncUIFromSettings(); refreshAppearance(); addStoreBlacklistButton(); refreshStoreBlacklistButtonState(); } function startCrossTabSync() { window.addEventListener('storage', (e) => { if (e.key !== SETTINGS_SYNC_KEY) return; applyRemoteSettings(); }); } function parseListInput(text) { return [...new Set(String(text || '').split(/[\n,]/).map(s => s.trim()).filter(Boolean))]; } const TRANSLIT = { 'ᴀ':'a', 'ʙ':'b', 'ᴄ':'c', 'ᴅ':'d', 'ᴇ':'e', 'ꜰ':'f', 'ɢ':'g', 'ʜ':'h', 'ɪ':'i', 'ᴊ':'j', 'ᴋ':'k', 'ʟ':'l', 'ᴍ':'m', 'ɴ':'n', 'ᴏ':'o', 'ᴘ':'p', 'ʀ':'r', 'ꜱ':'s', 'ᴛ':'t', 'ᴜ':'u', 'ᴠ':'v', 'ᴡ':'w', 'ʏ':'y', 'ᴢ':'z', 'ɐ':'a', 'ɑ':'a', 'ɒ':'o', 'ɔ':'o', 'ɓ':'b', 'ɕ':'c', 'ɖ':'d', 'ɗ':'d', 'ɘ':'e', 'ə':'e', 'ɚ':'er', 'ɛ':'e', 'ɜ':'e', 'ɝ':'er', 'ɞ':'o', 'ɟ':'j', 'ɡ':'g', 'ɥ':'h', 'ɦ':'h', 'ɨ':'i', 'ɬ':'l', 'ɭ':'l', 'ɮ':'l', 'ɯ':'u', 'ɰ':'u', 'ɱ':'m', 'ɲ':'n', 'ɳ':'n', 'ɵ':'o', 'ɹ':'r', 'ɺ':'r', 'ɻ':'r', 'ɼ':'r', 'ɽ':'r', 'ɾ':'r', 'ʁ':'r', 'ʂ':'s', 'ʃ':'sh', 'ʈ':'t', 'ʉ':'u', 'ʊ':'u', 'ʋ':'v', 'ʌ':'a', 'ʍ':'w', 'ʎ':'y', 'ʐ':'z', 'ʑ':'z', 'ʒ':'zh', 'ʔ':'', 'ʕ':'', 'ʡ':'', 'ʢ':'', 'ʰ':'h', 'ʲ':'j', 'ʷ':'w', 'ˀ':'', 'ˈ':'', 'ˌ':'', 'ː':'', 'ˑ':'', 'ʼ':'', 'å':'a', 'ø':'o', 'æ':'ae', 'œ':'oe', 'ß':'ss', 'ð':'d', 'þ':'th' }; function normalizeName(s) { return String(s || '') .replace(/[\p{S}]+/gu, ' ') .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .split('').map(ch => TRANSLIT[ch] ?? ch).join('') .split('.').map((tok, i, arr) => { if (i === arr.length - 1) return tok; const next = arr[i + 1]; if (tok && next && (tok.length === 1 || next.length === 1)) return tok + '.'; return tok + ' '; }).join('') .replace(/[^\p{L}\p{N}\s.]/gu, ' ') .replace(/[\s\u00A0]+/g, ' ') .replace(/^[.\s]+|[.\s]+$/g, '') .toLowerCase(); } function blacklistMatch(brand, entry) { const b = String(brand || '').trim(); const e = String(entry || '').trim(); if (!b || !e) return false; return b.toLowerCase() === e.toLowerCase() || normalizeName(b) === normalizeName(e); } const COLOR_WORDS = [ 'black', 'white', 'red', 'blue', 'green', 'pink', 'purple', 'yellow', 'orange', 'brown', 'tan', 'gray', 'grey', 'silver', 'gold', 'beige', 'cream', 'olive', 'mint', 'violet', 'burgundy', 'navy', 'teal', 'lavender', 'magenta', 'sky', 'lime' ]; function normalizeTitle(title) { let t = title.toLowerCase(); for (const color of COLOR_WORDS) { t = t.replace(new RegExp(`\\b${color}\\b`, 'g'), ''); } return t .replace(/\{.*?\}/g, '') .replace(/\(.*?\)/g, '') .replace(/\[.*?\]/g, '') .replace(/\s+/g, ' ') .trim(); } function normalizeForDemo(text) { return text.toLowerCase() .replace(/[^\p{L}\p{N}\s]/gu, ' ') .replace(/\s+/g, ' ') .trim(); } const DEMO_RE = /\bdemo(s)?\b/; function isDemo(name) { if (!name) return false; return DEMO_RE.test(normalizeForDemo(name)); } const BODY_ALIASES = [ { id: 'Reborn', label: 'Reborn/eBody', re: /\b(reborn|e\s?body)\b/ }, { id: 'Maitreya', label: 'Maitreya/Lara', re: /\b(maitreya|lara)\b/ }, { id: 'Legacy', label: 'Legacy', re: /\blegacy\b/ }, { id: 'Kupra', label: 'Kupra/Khupra', re: /\b(kupra|khupra)\b/ }, { id: 'Jake', label: 'Jake', re: /\bjake\b/ }, { id: 'Gianni', label: 'Gianni', re: /\bgianni\b/ }, ]; function detectBody(name) { if (!name) return null; const text = normalizeForDemo(name); for (const body of BODY_ALIASES) { if (body.re.test(text)) return body.id; } return null; } function shouldHideBody(body) { return !!(settings.collapseBodies && body && body !== settings.preferredBody); } function variantStem(title, stripColors) { let t = title.toLowerCase(); if (stripColors) { for (const color of COLOR_WORDS) { t = t.replace(new RegExp(`\\b${color}\\b`, 'g'), ' '); } t = t.replace(/\(.*?\)/g, ' '); } return t.replace(/[^\p{L}\p{N}]+/gu, ' ').replace(/\s+/g, ' ').trim(); } function computeGroupKey(item) { const body = settings.collapseBodies ? detectBody(item.name) : null; const stem = variantStem(item.title, settings.collapseColors); const demoFlag = isDemo(item.name) ? '|demo' : '|full'; return normalizeName(item.brand) + '|' + (body ? 'B' : 'C') + '|' + stem + demoFlag; } function decodeEntities(s) { if (!s) return ''; const el = document.createElement('textarea'); el.innerHTML = s; return el.value; } function slugToName(pathname) { const parts = pathname.split('/').filter(Boolean); const slug = parts.length >= 2 ? parts[parts.length - 2] : (parts[0] || ''); return decodeEntities(slug.replace(/-/g, ' ')); } function parseProductData(text) { const data = {}; const grab = (field) => { const m = text.match(new RegExp(`'${field}':\\s*'([^']*)'`)); return m ? m[1] : null; }; const name = grab('name'); const brand = grab('brand'); if (name !== null) data.name = decodeEntities(name).trim(); if (brand !== null) data.brand = decodeEntities(brand).trim(); return data; } const CARD_SELECTOR = 'div.gallery-item[id^="product-"]'; function getCards() { return [...document.querySelectorAll(CARD_SELECTOR)].map(card => { const script = card.querySelector('script'); const data = script ? parseProductData(script.textContent) : {}; const titleEl = card.querySelector('a.subtitle1'); let name = data.name; if (!name && titleEl) name = decodeEntities(titleEl.textContent.trim()); if (!name && titleEl && titleEl.href) { name = slugToName(new URL(titleEl.href, location.href).pathname); } const brandEl = card.querySelector('span.store-item'); const brand = data.brand || (brandEl ? brandEl.textContent.trim() : ''); const title = titleEl ? decodeEntities(titleEl.textContent.trim()) : ''; return { card, name, title: title || name, brand, href: titleEl ? titleEl.href : '' }; }).filter(item => item.name); } let lastSummary = null; function firstMatchingRule(item, storeCounts, variantReason) { const name = item.name.toLowerCase(); const brand = normalizeName(item.brand); if (settings.blacklistEnabled && item.brand && settings.blacklist.some(b => blacklistMatch(item.brand, b))) { return 'blacklist'; } if (settings.hideDemos && isDemo(item.name)) { return 'demo'; } if (settings.negativeKeywordsEnabled && settings.negativeKeywords.some(kw => kw && name.includes(kw.toLowerCase()))) { return 'negative keyword'; } if (shouldHideBody(item.body)) { return 'body filter'; } if (settings.collapseColors && variantReason) { return variantReason; } if (settings.maxPerStore > 0) { const count = storeCounts.get(brand) || 0; if (count >= settings.maxPerStore) { return 'store limit'; } storeCounts.set(brand, count + 1); } return null; } function applyFilters() { const items = getCards(); const storeCounts = new Map(); const variantReasons = new Map(); const summary = { total: items.length, hidden: 0, reasons: {} }; for (const item of items) { item.body = settings.collapseBodies ? detectBody(item.name) : null; } if (settings.collapseColors) { const groups = new Map(); for (const item of items) { const key = computeGroupKey(item); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(item); } for (const group of groups.values()) { if (group.length <= 1) continue; const winner = group[0]; for (const item of group) { if (item !== winner) { variantReasons.set(item, 'color variant'); } } } } for (const item of items) { item.card.style.display = ''; const reason = firstMatchingRule(item, storeCounts, variantReasons.get(item)); if (reason) { item.card.style.display = 'none'; summary.hidden += 1; summary.reasons[reason] = (summary.reasons[reason] || 0) + 1; if (settings.debug) { console.info('[SLMC] hidden → ' + reason + ':', item.name, '(', item.brand, ')'); } } } lastSummary = summary; if (settings.debug) { console.info('[SLMC] summary:', summary); } updateBadge(); return summary; } function addToBlacklist(storeName) { const list = [...new Set([...(settings.blacklist || []), String(storeName || '').trim()].filter(Boolean))]; setSetting('blacklist', list); if (settings.debug) { console.info('[SLMC] blacklisted store:', storeName); } } function removeFromBlacklist(storeName) { const list = (settings.blacklist || []).filter(entry => !blacklistMatch(storeName, entry)); setSetting('blacklist', list); if (settings.debug) { console.info('[SLMC] un-blacklisted store:', storeName); } } function refreshStoreBlacklistButtonState() { const btn = document.querySelector('button.slmc-store-bl-btn'); if (!btn) return; const titleEl = document.querySelector('#merchant-details .merchant-title h5'); const storeName = titleEl ? titleEl.textContent.trim() : ''; if (!storeName) return; const isBlacklisted = (settings.blacklist || []).some(entry => blacklistMatch(storeName, entry)); btn.classList.toggle('slmc-bl-btn-done', isBlacklisted); btn.textContent = isBlacklisted ? 'Remove from Blacklist' : '⊘ Blacklist this store'; btn.title = isBlacklisted ? 'Remove this store from the blacklist' : 'Add this store to the blacklist'; } function addStoreBlacklistButton() { if (!settings.blacklistEnabled) return; const profile = document.querySelector('.merchant-profile'); if (!profile) return; const favLink = profile.querySelector('a.profile-detail-link[href^="/favorite_stores"]'); if (!favLink) return; ensureStyle(STORE_BTN_CSS, 'slmc-store-style'); if (profile.querySelector('.slmc-store-bl-btn')) return; const titleEl = document.querySelector('#merchant-details .merchant-title h5'); const storeName = titleEl ? titleEl.textContent.trim() : ''; if (!storeName) return; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'slmc-store-bl-btn'; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const isBlacklisted = (settings.blacklist || []).some(entry => blacklistMatch(storeName, entry)); if (isBlacklisted) { removeFromBlacklist(storeName); } else { addToBlacklist(storeName); } }); favLink.insertAdjacentElement('afterend', btn); refreshStoreBlacklistButtonState(); } function convertPrice(linden) { const usd = linden * settings.usdPerLinden; switch (settings.currency) { case 'EUR': return '€' + (usd * settings.eurPerUsd).toFixed(2); case 'USD': return '$' + usd.toFixed(2); default: return '$' + usd.toFixed(2); } } function removeConvertedPrices() { document.querySelectorAll('.slmc-converted').forEach(el => el.remove()); } function addConvertedPrice(el) { if (el.querySelector('.slmc-converted')) return; const match = el.textContent.match(/L\$\s*([\d.,\s]+)/); if (!match) return; const value = parseInt(match[1].replace(/[\s.,]/g, ''), 10); const span = document.createElement('span'); span.className = 'slmc-converted'; span.textContent = ' (' + convertPrice(value) + ')'; el.appendChild(span); } function addConvertedPrices() { const els = new Set([ ...document.querySelectorAll('.title4'), ...document.querySelectorAll('.price-ld'), ]); els.forEach(addConvertedPrice); } function applyPrices() { if (settings.showCurrency) { addConvertedPrices(); } else { removeConvertedPrices(); } } function setNativeCheckbox(name, checked) { const el = document.querySelector(`input[name="${name}"]`); if (el && el.type === 'checkbox' && el.checked !== checked) { el.checked = checked; } } function applyMarketplaceFilters() { if (settings.hideLimited) { setNativeCheckbox('no_quantity', true); } } function refreshAll() { applyMarketplaceFilters(); applyFilters(); applyPrices(); } function updateTitle() { const params = new URLSearchParams(location.search); const q = params.get('q') || params.get('search[keywords]'); document.title = q ? 'SL MP - ' + q : 'SL MP - ' + document.title.slice(26); } const UI_ID = 'slmc-ui'; const UI_ROWS = [ { key: 'hideDemos', type: 'bool', label: 'Hide demos (title)' }, { key: 'hideLimited', type: 'bool', label: 'Hide limited quantity' }, { key: 'collapseColors', type: 'bool', label: 'Collapse color variants' }, { key: 'collapseBodies', type: 'bool', label: 'Filter to preferred body' }, { key: 'preferredBody', type: 'select', label: 'Preferred body', options: [ ['Reborn', 'Reborn/eBody'], ['Maitreya', 'Maitreya/Lara'], ['Legacy', 'Legacy'], ['Kupra', 'Kupra/Khupra'], ['Jake', 'Jake'], ['Gianni', 'Gianni'] ] }, { key: 'showCurrency', type: 'bool', label: 'Show converted prices' }, { key: 'blacklist', type: 'list', label: 'Blacklist stores', placeholder: 'exact store names, one per line' }, { key: 'negativeKeywords', type: 'list', label: 'Negative keywords', placeholder: 'title substrings, e.g. gacha' }, { key: 'maxPerStore', type: 'select', label: 'Max per store', options: [['-1', 'Off'], ['1', '1'], ['2', '2'], ['3', '3'], ['5', '5'], ['10', '10']] }, { key: 'layoutEnabled', type: 'bool', label: 'Full-width layout' }, { key: 'debug', type: 'bool', label: 'Debug log' }, ]; const UI_CSS = ` #slmc-ui { position: relative; display: inline-block; } #slmc-ui .slmc-menu { position: absolute; top: calc(100% + 4px); left: 0; width: 280px !important; min-width: 280px !important; max-width: 340px !important; margin: 0; padding: 6px 0; list-style: none; box-sizing: border-box; background: #ffffff; border: 1px solid #d9dde3; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); z-index: 10000; overflow: visible; } #slmc-ui .slmc-menu[aria-hidden="true"] { display: none !important; } #slmc-ui .slmc-menu[aria-hidden="false"] { display: block !important; } #slmc-ui .slmc-row { display: block; } #slmc-ui .slmc-row label { display: flex; align-items: center; gap: 10px; padding: 8px 16px; cursor: pointer; white-space: nowrap; font-size: 13px; color: #333; } #slmc-ui .slmc-row label:hover { background: rgba(0, 0, 0, 0.05); } #slmc-ui .slmc-row input[type="checkbox"] { margin: 0; flex: 0 0 auto; } #slmc-ui .slmc-row-select { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 16px; font-size: 13px; white-space: nowrap; color: #333; } #slmc-ui .slmc-row-select select { max-width: 90px; } #slmc-ui .slmc-row-list { padding: 8px 16px; } #slmc-ui .slmc-row-list-head { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #333; white-space: nowrap; } #slmc-ui .slmc-row-list .slmc-row-list-enable { display: flex; align-items: center; gap: 10px; cursor: pointer; margin: 0; padding: 0; } #slmc-ui .slmc-row-list-enable input[type="checkbox"] { margin: 0; } #slmc-ui .slmc-row-list-head .slmc-badge { margin-left: auto; } #slmc-ui .slmc-row-list.slmc-list-disabled { opacity: 0.5; } #slmc-ui .slmc-list-toggle, #slmc-ui .slmc-list-btn { background: #f0f2f4; border: 1px solid #d9dde3; border-radius: 4px; padding: 2px 8px; font-size: 12px; cursor: pointer; color: #333; } #slmc-ui .slmc-list-toggle:hover, #slmc-ui .slmc-list-btn:hover { background: #e2e6ea; } #slmc-ui .slmc-list-editor { margin-top: 6px; } #slmc-ui .slmc-list-editor textarea { width: 100%; box-sizing: border-box; font-size: 12px; padding: 4px 6px; border: 1px solid #d9dde3; border-radius: 4px; resize: vertical; color: #333; background: #fff; } #slmc-ui .slmc-list-actions { display: flex; gap: 6px; margin-top: 6px; } #slmc-ui .slmc-badge { margin-left: 6px; min-width: 18px; padding: 0 5px; border-radius: 9px; font-size: 11px; line-height: 18px; text-align: center; background: #e2e6ea; color: #4a5560; } #slmc-ui .slmc-badge-active { background: #0178BF; color: #fff; } `; const STORE_BTN_CSS = ` button.slmc-store-bl-btn { display: block; width: 100%; margin: 8px 0 0; padding: 6px 10px; border: 1px solid #c5ccd3 !important; border-radius: 3px; background: #fafbfc !important; color: #5a6672 !important; font-size: 12px; font-weight: 400; line-height: 16px; box-sizing: border-box; appearance: none; -webkit-appearance: none; cursor: pointer; text-align: center; text-decoration: none; } button.slmc-store-bl-btn:hover { background: #e2e6ea !important; color: #5a6672 !important; } button.slmc-store-bl-btn.slmc-bl-btn-done, button.slmc-store-bl-btn.slmc-bl-btn-done:hover { background: #fafbfc !important; border-color: #c5ccd3 !important; color: #5a6672 !important; } `; const LAYOUT_CSS = ` html.slmc-layout { --max-page-width: 100vw; } html.slmc-layout #body-shadow-repeating { width: auto !important; max-width: none !important; margin: 0 !important; } html.slmc-layout #canvas #merchant-banner { float: none !important; width: auto !important; margin-right: 0 !important; } html.slmc-layout #centered-page { width: auto !important; max-width: none !important; margin: 0 !important; box-shadow: none !important; } html.slmc-layout #canvas { margin-left: 24px !important; margin-right: 24px !important; } html.slmc-layout #canvas #main-content { width: auto !important; max-width: none !important; } html.slmc-layout #canvas #search-results-container { width: auto !important; max-width: none !important; float: none !important; margin-left: 240px !important; display: flow-root !important; } html.slmc-layout .search-results-container { flex: 1 1 0 !important; min-width: 0 !important; max-width: none !important; float: none !important; margin-left: 0 !important; display: flow-root !important; } html.slmc-layout #search-menu.search-menu { width: 220px !important; margin-right: 20px !important; } html.slmc-layout #search-results-container .product-listing.gallery, html.slmc-layout .search-results-container .product-listing.gallery { width: 100% !important; max-width: none !important; } html.slmc-layout .search-results-container .product-listing.gallery { display: block !important; } html.slmc-layout .search-results-container .gallery-item-container { gap: 10px 8px !important; justify-content: flex-start !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item { width: 220px !important; height: 268px !important; max-width: 220px !important; flex-shrink: 0 !important; margin: 0 !important; border: 1px solid #ccc !important; border-radius: 0 !important; background: #fff !important; overflow: hidden !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item:hover { border-color: #ccc !important; box-shadow: none !important; transform: none !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .product-image { display: block !important; text-align: center !important; width: 220px !important; height: 165px !important; margin-bottom: 10px !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .product-image img { width: 100% !important; height: 100% !important; max-width: 220px !important; max-height: 165px !important; object-fit: contain !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .item-description-container { padding: 0 5px 10px 5px !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .item-description-container .item-description { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-top: 6px !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .item-description-container .subtitle1 { overflow-wrap: break-word !important; font-weight: bold !important; line-height: 100% !important; height: 27px !important; font-size: 14px !important; display: block !important; overflow: hidden !important; color: var(--color-black) !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .item-description-container .body2 { margin-top: 6px !important; height: 20px !important; overflow: hidden !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .item-description-container .item-description .title4 { font-weight: bold !important; font-size: 15px !important; } html.slmc-layout .search-results-container .gallery-item-container .gallery-item .item-description-container .item-description .product-review-stars { display: inline-flex !important; align-items: center !important; vertical-align: middle !important; } html.slmc-layout .search-results-container .featured-items-carousel .gallery-item-container .gallery-item { height: auto !important; } html.slmc-layout .search-results-container .featured-items-carousel { width: 100% !important; max-width: none !important; grid-column: 1 / -1 !important; } html.slmc-layout .search-results-container .featured-items-carousel .carousel-container { width: 100% !important; max-width: none !important; } html.slmc-layout .search-results-container .featured-items-carousel .gallery-item-container { gap: 10px 8px !important; } html.slmc-layout .sidebar-container { font-size: 12px !important; line-height: 16px !important; } html.slmc-layout .sidebar-container h4 { margin-bottom: 8px !important; } html.slmc-layout .sidebar-container .category-link a, html.slmc-layout .sidebar-container .category-link span, html.slmc-layout .sidebar-container .checkbox-container label, html.slmc-layout .sidebar-container label, html.slmc-layout .sidebar-container .subtitle1 { font-size: 12px !important; } html.slmc-layout .sidebar-container .category-link svg { width: 18px !important; height: 18px !important; } html.slmc-layout #merchant-metadata .merchant-photo img { width: 32px !important; height: 32px !important; } html.slmc-layout #merchant-metadata .merchant-title h5 { font-size: 14px !important; } html.slmc-layout #merchant-metadata .merchant-profile dt { font-size: 12px !important; padding-left: 8px !important; } `; const NIGHT_CSS = ` html.slmc-night { color-scheme: dark; } html.slmc-night body, html.slmc-night #centered-page, html.slmc-night #canvas-container, html.slmc-night #canvas, html.slmc-night #main-content, html.slmc-night .layout__wrapper, html.slmc-night .layout__main { background-color: #15181e !important; color: #e6e9ef !important; } html.slmc-night a { color: #52c4ff !important; } html.slmc-night a:visited { color: #9aa2b1 !important; } html.slmc-night .product-listing.gallery .gallery-item, html.slmc-night .gallery-item-container .gallery-item, html.slmc-night .home-card, html.slmc-night .home-featured { background-color: #22262f !important; border-color: #3a404c !important; color: #e6e9ef !important; } html.slmc-night .search-results-container .gallery-item-container .gallery-item, html.slmc-night .featured-items-carousel .gallery-item-container .gallery-item { background-color: #22262f !important; border-color: #3a404c !important; } html.slmc-night .featured-items-carousel { background-color: #1b1f27 !important; } html.slmc-night .gallery-item .subtitle1, html.slmc-night .gallery-item .item-description-container .body2, html.slmc-night .gallery-item .store-item, html.slmc-night .sidebar-container, html.slmc-night .filter-options, html.slmc-night .range-container, html.slmc-night .breadcrumb, html.slmc-night .breadcrumb a, html.slmc-night .footer-paginate a, html.slmc-night .footer-paginate span { color: #cfd6e0 !important; } html.slmc-night .gallery-item .title4, html.slmc-night .item-description-container .item-description .title4, html.slmc-night .gallery-item a.product-title, html.slmc-night .item-description-container .subtitle1, html.slmc-night .item-description a, html.slmc-night #merchant-metadata .merchant-title h5, html.slmc-night .merchant-profile p, html.slmc-night .sidebar-container a, html.slmc-night .sorting-container, html.slmc-night .search-results-headers h1 { color: #e6e9ef !important; } html.slmc-night .sidebar-container .category-count { color: #9aa2b1 !important; } html.slmc-night #merchant-metadata .merchant-profile dt { color: #cfd6e0 !important; } html.slmc-night #marketplace-toolbar, html.slmc-night .layout__header, html.slmc-night .search-desktop, html.slmc-night .marketplace-tabs { color: #e6e9ef !important; } html.slmc-night .marketplace-tabs, html.slmc-night .marketplace-tabs .tab-container, html.slmc-night .marketplace-tabs .tab-headers, html.slmc-night .marketplace-tabs .sidebar-content, html.slmc-night .marketplace-tabs .sidebar, html.slmc-night .range-container, html.slmc-night .range-container .form-group { background-color: #1b1f27 !important; border-color: #3a404c !important; } html.slmc-night .marketplace-tabs .tab-header.selected { background-color: #2a2f3a !important; } html.slmc-night .tab-header { color: #9aa2b1 !important; } html.slmc-night .tab-header.selected { color: #e6e9ef !important; } html.slmc-night .search-input input, html.slmc-night input[type="text"], html.slmc-night input[type="search"], html.slmc-night input[type="number"], html.slmc-night textarea, html.slmc-night select { background-color: #22262f !important; color: #e6e9ef !important; border-color: #3a404c !important; } html.slmc-night input[type="checkbox"] { accent-color: #52c4ff; } html.slmc-night .popupmenu-button, html.slmc-night .sorting-container .popupmenu-button, html.slmc-night .result-sort-header-desktop .popupmenu-button { background: transparent !important; color: #e6e9ef !important; border-color: #3a404c !important; } html.slmc-night .menu { background-color: #22262f !important; border-color: #3a404c !important; } html.slmc-night .menu button { background: transparent !important; color: #e6e9ef !important; } html.slmc-night .marketplace-cart a { color: #e6e9ef !important; } html.slmc-night #slmc-ui .slmc-menu { background: #22262f !important; border-color: #3a404c !important; } html.slmc-night #slmc-ui .slmc-row label, html.slmc-night #slmc-ui .slmc-row-select, html.slmc-night #slmc-ui .slmc-row-list-head { color: #e6e9ef !important; } html.slmc-night #slmc-ui .slmc-list-editor textarea { background: #15181e !important; color: #e6e9ef !important; border-color: #3a404c !important; } html.slmc-night button.slmc-store-bl-btn { background: #22262f !important; border-color: #3a404c !important; color: #cfd6e0 !important; } html.slmc-night button.slmc-store-bl-btn:hover { background: #2a2f3a !important; color: #e6e9ef !important; } `; const NAV_TOGGLE_CSS = ` button.slmc-nav-toggle { display: inline-flex !important; align-items: center; justify-content: center; margin-left: 15px; padding: 4px 7px; background: transparent !important; border: none !important; border-radius: 4px; color: #b6b6b6 !important; line-height: 0; cursor: pointer; box-shadow: none !important; appearance: none; -webkit-appearance: none; font-size: 14px; } button.slmc-nav-toggle:hover { background: rgba(255, 255, 255, 0.08) !important; color: #ffffff !important; } button.slmc-nav-toggle:focus { outline: 1px solid #00bfff; background: rgba(255, 255, 255, 0.08) !important; } button.slmc-nav-toggle.slmc-nav-toggle-on { color: #00bfff !important; } button.slmc-nav-toggle svg { display: block; } `; const NAV_LAYOUT_ICON = ''; const NAV_LAYOUT_OFF_ICON = ''; let uiBuilt = false; function buildRow(row) { const li = document.createElement('li'); li.className = 'slmc-row'; if (row.type === 'bool') { const label = document.createElement('label'); const input = document.createElement('input'); input.type = 'checkbox'; input.dataset.setting = row.key; const text = document.createElement('span'); text.textContent = row.label; label.appendChild(input); label.appendChild(text); li.appendChild(label); } else if (row.type === 'select') { li.classList.add('slmc-row-select'); const text = document.createElement('span'); text.textContent = row.label; const select = document.createElement('select'); select.dataset.setting = row.key; for (const [value, label] of row.options) { const opt = document.createElement('option'); opt.value = value; opt.textContent = label; select.appendChild(opt); } li.appendChild(text); li.appendChild(select); } else if (row.type === 'list') { li.classList.add('slmc-row-list'); const head = document.createElement('div'); head.className = 'slmc-row-list-head'; const enable = document.createElement('label'); enable.className = 'slmc-row-list-enable'; const toggleInput = document.createElement('input'); toggleInput.type = 'checkbox'; toggleInput.dataset.setting = row.key + 'Enabled'; const text = document.createElement('span'); text.textContent = row.label; enable.appendChild(toggleInput); enable.appendChild(text); const badge = document.createElement('span'); badge.className = 'slmc-badge'; badge.dataset.listBadge = row.key; badge.textContent = '0'; const editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.className = 'slmc-list-toggle'; editBtn.dataset.listToggle = row.key; editBtn.textContent = 'Edit'; head.appendChild(enable); head.appendChild(badge); head.appendChild(editBtn); li.appendChild(head); const editor = document.createElement('div'); editor.className = 'slmc-list-editor'; editor.dataset.listEditor = row.key; editor.hidden = true; const ta = document.createElement('textarea'); ta.rows = 5; ta.placeholder = row.placeholder; const actions = document.createElement('div'); actions.className = 'slmc-list-actions'; const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'slmc-list-btn'; saveBtn.dataset.listSave = row.key; saveBtn.textContent = 'Save'; const cancelBtn = document.createElement('button'); cancelBtn.type = 'button'; cancelBtn.className = 'slmc-list-btn'; cancelBtn.dataset.listCancel = row.key; cancelBtn.textContent = 'Cancel'; actions.appendChild(saveBtn); actions.appendChild(cancelBtn); editor.appendChild(ta); editor.appendChild(actions); li.appendChild(editor); } return li; } function wireUI(root, button, menu) { function close() { menu.setAttribute('aria-hidden', 'true'); button.setAttribute('aria-expanded', 'false'); } function open() { document.querySelectorAll('.slmc-menu[aria-hidden="false"]').forEach(other => { other.setAttribute('aria-hidden', 'true'); const b = document.querySelector(`[aria-controls="${other.id}"]`); if (b) b.setAttribute('aria-expanded', 'false'); }); menu.setAttribute('aria-hidden', 'false'); button.setAttribute('aria-expanded', 'true'); } button.addEventListener('click', (e) => { e.stopPropagation(); if (button.getAttribute('aria-expanded') === 'true') close(); else open(); }); menu.addEventListener('click', (e) => e.stopPropagation()); menu.addEventListener('change', (e) => { const input = e.target.closest('[data-setting]'); if (!input) return; const value = input.type === 'checkbox' ? input.checked : input.value; setSetting(input.dataset.setting, value); }); menu.addEventListener('click', (e) => { const toggle = e.target.closest('[data-list-toggle]'); const save = e.target.closest('[data-list-save]'); const cancel = e.target.closest('[data-list-cancel]'); if (!toggle && !save && !cancel) return; const li = e.target.closest('.slmc-row-list'); const editor = li.querySelector('.slmc-list-editor'); const ta = editor.querySelector('textarea'); if (toggle) { if (editor.hidden) { ta.value = (settings[toggle.dataset.listToggle] || []).join('\n'); editor.hidden = false; toggle.textContent = 'Close'; ta.focus(); } else { editor.hidden = true; toggle.textContent = 'Edit'; } } else if (save) { setSetting(save.dataset.listSave, parseListInput(ta.value)); editor.hidden = true; li.querySelector('[data-list-toggle]').textContent = 'Edit'; } else if (cancel) { editor.hidden = true; li.querySelector('[data-list-toggle]').textContent = 'Edit'; } }); document.addEventListener('click', (e) => { if (!root.contains(e.target)) close(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); } function ensureStyle(css, id) { if (document.getElementById(id)) return; const style = document.createElement('style'); style.id = id; style.textContent = css; document.head.appendChild(style); } function isLayoutPage() { const p = location.pathname; return p.startsWith('/products/search') || /^\/stores\/\d+(\/|$)/.test(p); } function ensureLayoutStyles() { ensureStyle(LAYOUT_CSS, 'slmc-layout-style'); ensureStyle(NIGHT_CSS, 'slmc-night-style'); ensureStyle(NAV_TOGGLE_CSS, 'slmc-nav-style'); } function applyLayoutClass() { document.documentElement.classList.toggle('slmc-layout', isLayoutPage() && !!settings.layoutEnabled); } function refreshAppearance() { applyLayoutClass(); document.documentElement.classList.remove('slmc-night'); updateNavToggleButtons(); } function updateNavToggleButtons() { const layoutBtn = document.getElementById('slmc-layout-toggle'); if (layoutBtn) { const on = !!settings.layoutEnabled; layoutBtn.classList.toggle('slmc-nav-toggle-on', on); layoutBtn.innerHTML = on ? NAV_LAYOUT_ICON : NAV_LAYOUT_OFF_ICON; layoutBtn.title = on ? 'Full-width layout: ON' : 'Full-width layout: OFF'; } } function addNavToggles() { const header = document.querySelector('nav.header'); if (!header) return; ensureLayoutStyles(); const existing = document.getElementById('slmc-layout-toggle'); if (existing && existing.isConnected) return; ['slmc-layout-toggle', 'slmc-theme-toggle'].forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); const layoutBtn = document.createElement('button'); layoutBtn.type = 'button'; layoutBtn.id = 'slmc-layout-toggle'; layoutBtn.className = 'slmc-nav-toggle'; layoutBtn.innerHTML = NAV_LAYOUT_ICON; layoutBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); setSetting('layoutEnabled', !settings.layoutEnabled); }); const host = header.querySelector('.nav__signed-in, .nav__signed-out') || header; if (host === header) { header.appendChild(layoutBtn); } else { const buyLd = header.querySelector('#navbar_buyld'); if (buyLd) { host.insertBefore(layoutBtn, buyLd); } else { host.insertBefore(layoutBtn, host.firstChild); } } updateNavToggleButtons(); } function injectUI() { if (uiBuilt) return; const container = document.querySelector('.sorting-container'); if (!container) return; const root = document.createElement('div'); root.className = 'popupmenu-container slmc-ui'; root.id = UI_ID; const button = document.createElement('button'); button.type = 'button'; button.className = 'popupmenu-button reset-button'; button.id = 'slmc-ui-btn'; button.setAttribute('aria-haspopup', 'true'); button.setAttribute('aria-expanded', 'false'); button.title = 'SL Marketplace Cleaner settings'; const icon = document.createElement('span'); icon.innerHTML = ''; const label = document.createElement('span'); label.className = 'label2'; label.textContent = 'SL Cleaner'; const badge = document.createElement('span'); badge.className = 'slmc-badge'; badge.id = 'slmc-badge'; badge.textContent = '0'; badge.title = 'Items hidden on this page'; button.appendChild(icon); button.appendChild(label); button.appendChild(badge); const menu = document.createElement('ul'); menu.className = 'menu slmc-menu'; menu.id = 'slmc-menu'; menu.setAttribute('role', 'menu'); menu.setAttribute('aria-hidden', 'true'); for (const row of UI_ROWS) { menu.appendChild(buildRow(row)); } root.appendChild(button); root.appendChild(menu); container.insertBefore(root, container.firstChild); ensureStyle(UI_CSS, 'slmc-ui-style'); uiBuilt = true; wireUI(root, button, menu); syncUIFromSettings(); updateBadge(); } function syncListBadges() { if (!uiBuilt) return; document.querySelectorAll('#slmc-ui [data-list-badge]').forEach(badge => { const key = badge.dataset.listBadge; const n = (settings[key] || []).length; badge.textContent = String(n); badge.classList.toggle('slmc-badge-active', n > 0); }); } function syncUIFromSettings() { if (!uiBuilt) return; document.querySelectorAll('#slmc-ui [data-setting]').forEach(el => { const key = el.dataset.setting; if (el.type === 'checkbox') { el.checked = !!settings[key]; } else if (el.type === 'select-one') { el.value = String(settings[key]); } }); syncListBadges(); document.querySelectorAll('#slmc-ui .slmc-row-list').forEach(li => { const cb = li.querySelector('input[type="checkbox"]'); li.classList.toggle('slmc-list-disabled', cb ? !cb.checked : false); }); const prefSelect = document.querySelector('#slmc-ui select[data-setting="preferredBody"]'); if (prefSelect) prefSelect.disabled = !settings.collapseBodies; } function updateBadge() { if (!uiBuilt) return; const badge = document.getElementById('slmc-badge'); if (!badge) return; const n = lastSummary ? lastSummary.hidden : 0; badge.textContent = String(n); badge.classList.toggle('slmc-badge-active', n > 0); } function ensureUI() { const container = document.querySelector('.sorting-container'); if (!container) return; const existing = document.getElementById(UI_ID); if (existing && existing.isConnected) return; uiBuilt = false; injectUI(); } let filterTimer = null; function onDomChange() { clearTimeout(filterTimer); filterTimer = setTimeout(() => { ensureUI(); applyFilters(); addStoreBlacklistButton(); addNavToggles(); }, 50); } const observer = new MutationObserver((mutations) => { if (mutations.some(m => m.addedNodes.length > 0)) { onDomChange(); } }); function init() { startCrossTabSync(); refreshAppearance(); ensureLayoutStyles(); updateTitle(); applyMarketplaceFilters(); applyFilters(); applyPrices(); ensureUI(); addStoreBlacklistButton(); addNavToggles(); } let initialized = false; function boot() { if (initialized) return; initialized = true; const start = () => { if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } init(); }; if (document.readyState === 'loading') { window.addEventListener('load', start); } else { start(); } } window.__SLMC = { isDemo, detectBody, shouldHideBody, variantStem, computeGroupKey, normalizeForDemo, normalizeTitle, slugToName, parseProductData, parseListInput, normalizeName, blacklistMatch, getSettings: () => ({ ...settings }), refreshAppearance, addNavToggles, updateNavToggleButtons, applyRemoteSettings, notifySettingsChanged, }; boot(); })();