您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A browsing companion for traderie.com with bookmarking, adblock, and Diablo 2 price history
// ==UserScript== // Filename: traderie-tools.user.js // ==UserScript== // @name Traderie Tools // @namespace http://tampermonkey.net/ // @version 2025-05v2 // @description A browsing companion for traderie.com with bookmarking, adblock, and Diablo 2 price history // @match *://*.traderie.com/* // @grant GM_xmlhttpRequest // @connect raw.githubusercontent.com // @license MIT // ==/UserScript== (function() { 'use strict'; /********************** * Traderie Adblocker * **********************/ const STORAGE_KEY = 'traderie-adblock-groups'; const ENABLED_KEY = 'traderie-adblock-enabled'; let adBlockObserver = null; const SELECTOR_GROUPS = { google: ['.GoogleActiveViewElement', '[id^="google_ads_iframe"]', 'iframe[src*="googlesyndication"]', 'iframe[src*="2mdn"]'], generic: ['[id*="anchor"]', '[id*="ad_unit"]', '[data-ad^="leaderboard-"]', 'div[data-ad="left-rail-3"]'], video: ['iframe[src*="anyclip"]', 'video[id^="ac-lre-vjs-"]', '.ac-player-wrapper'], styled: ['div.sc-gfoqjT.gXykUj', 'div[class*="gfoqjT"]', 'div.ns-08pl9-l-square-gmb', 'div.sc-eyvILC.pvcVG', '.sc-kbousE.dRxaoW', 'div.sc-bpUBKd.jVYraK.cool-slot'], tracking: ['script[src*="doubleverify"]'], misc: ['a[href="/akrewpro"]', 'span[style*="justify-content: space-between"]', 'svg[style*="left: 160px"]', 'svg[style*="right: 160px"]', 'div.container > div.banner-slider'] }; const DEFAULT_ENABLED_GROUPS = Object.keys(SELECTOR_GROUPS); const CRITICAL_SELECTORS = ['html', 'head', 'body', 'main', 'nav', '[class*="app"]', '[class*="root"]', '[class*="page"]', '[class*="content"]', '[class*="listing"]', '[class*="trade"]', '[class*="navigation"]', '[class*="header"]', '[class*="menu"]', '[class*="sidebar"]']; const BLOCKLIST_EXCEPTIONS = ['.listing-row', '.listing-wrapper', '.listing-container', '#listing-root', '[class*="listing"]', '.sc-etKGGb']; function isAdblockEnabled() { return localStorage.getItem(ENABLED_KEY) !== 'false'; } function setAdblockEnabled(val) { localStorage.setItem(ENABLED_KEY, val ? 'true' : 'false'); } function getEnabledGroups() { const stored = localStorage.getItem(STORAGE_KEY); return stored ? JSON.parse(stored) : DEFAULT_ENABLED_GROUPS; } function getActiveSelectors() { return getEnabledGroups().flatMap(group => SELECTOR_GROUPS[group] || []); } function injectAdBlockCSS() { const style = document.createElement('style'); style.id = 'traderie-instant-adblock'; style.textContent = getActiveSelectors().map(sel => `${sel} { display: none !important; }`).join('\n'); document.head?.appendChild(style); } function isCriticalElement(el) { if (!el || !el.matches) return true; try { return CRITICAL_SELECTORS.some(sel => el.matches(sel)); } catch { return true; } } function removeEmptyAncestors(el) { let parent = el?.parentNode; while (parent && parent !== document.body) { if (parent.children.length === 0 && parent.textContent.trim() === '') { const next = parent.parentNode; parent.remove(); parent = next; } else { break; } } } function safeRemoveStyled(el) { if (!el || !el.parentNode) return false; const container = el.closest('[class*="ad"], [id*="ad"], [data-ad]') || el; if (BLOCKLIST_EXCEPTIONS.some(skip => container.matches?.(skip))) return false; if (isCriticalElement(container)) return false; try { container.remove(); removeEmptyAncestors(container); return true; } catch { return false; } } function removeAdsFromContainer(container = document.body) { const selectors = getActiveSelectors(); selectors.forEach(selector => { try { container.querySelectorAll(selector).forEach(el => { safeRemoveStyled(el); }); } catch {} }); container.querySelectorAll('div.sc-gfoqjT.gXykUj').forEach(el => { if (el.textContent?.trim() === 'Traderie is supported by ads') { const wrapper = el.closest('.sc-eqUAAy.sc-eyvILC'); if (wrapper && !isCriticalElement(wrapper)) { wrapper.remove(); } } }); } function performInitialCleanup() { setTimeout(() => removeAdsFromContainer(), 500); setTimeout(() => removeAdsFromContainer(), 1000); setTimeout(() => removeAdsFromContainer(), 1500); } function startAdblockObserver() { adBlockObserver = new MutationObserver(mutations => { mutations.forEach(m => { m.addedNodes.forEach(n => { if (n.nodeType === 1) { if (getActiveSelectors().some(sel => n.matches?.(sel))) { safeRemoveStyled(n); } else { removeAdsFromContainer(n); } } }); }); }); adBlockObserver.observe(document.body, { childList: true, subtree: true }); } function initAdblocker() { injectAdBlockCSS(); performInitialCleanup(); setTimeout(startAdblockObserver, 200); setInterval(() => removeAdsFromContainer(), 5000); } function disableAdblocker() { document.getElementById('traderie-instant-adblock')?.remove(); adBlockObserver?.disconnect(); } window.traderieAdblock = { enable: () => { setAdblockEnabled(true); initAdblocker(); }, disable: () => { setAdblockEnabled(false); disableAdblocker(); }, isEnabled: isAdblockEnabled }; /******************************* * Rune Pricing Functionality * *******************************/ const PRICE_URL = 'https://raw.githubusercontent.com/wguDataNinja/TraderieTools/main/rune_prices.json'; const RUNE_ENABLED_KEY = 'traderie-rune-pricing-enabled'; let runePrices = null; let runeObserver = null; function isRunePricingEnabled() { return localStorage.getItem(RUNE_ENABLED_KEY) === 'true'; } function setRunePricingEnabled(val) { localStorage.setItem(RUNE_ENABLED_KEY, val ? 'true' : 'false'); } function getServerSlug() { const p = new URLSearchParams(window.location.search); const plat = (p.get('prop_Platform') || 'pc').toLowerCase(); const mode = p.get('prop_Mode') === 'hardcore' ? 'hc' : 'sc'; const lad = p.get('prop_Ladder') === 'true' ? 'l' : 'nl'; return `${plat}_${mode}_${lad}`; } function parseRune(el) { if (!el) return null; const c = el.cloneNode(true); Array.from(c.children).forEach(ch => c.removeChild(ch)); const m = c.textContent.trim().match(/(\d+)\s*[xX]\s*(.+)/); return m ? { quantity: +m[1], item: m[2].trim() } : null; } function parseAskGroups(container) { const lines = [...container.querySelectorAll('.price-line, .tooltiptext .price-line')]; const groups = []; let curr = []; lines.forEach((ln, i) => { const txt = ln.textContent.trim().toUpperCase(); const or = txt === 'OR'; const items = [...ln.querySelectorAll('a')].map(parseRune).filter(Boolean); if (items.length) curr.push(...items); if (or || (i + 1 < lines.length && lines[i + 1].textContent.trim().toUpperCase() === 'OR')) { if (curr.length) { groups.push({ items: curr, element: ln }); curr = []; } } }); if (curr.length) groups.push({ items: curr, element: lines[lines.length - 1] }); return groups; } function buildTooltipText(offer, asks, prices, slug) { const val = r => prices[slug]?.[r.item]?.ist_value ?? null; const oVal = val(offer); if (oVal == null) return ''; const askLines = asks.map(r => { const v = val(r); return `${r.quantity} x ${r.item} (${v != null ? (r.quantity * v).toFixed(2) : '--'} Ist)`; }); const total = asks.every(r => val(r) != null) ? asks.reduce((s, r) => s + r.quantity * val(r), 0).toFixed(2) : '--'; return `Offer: ${offer.quantity} x ${offer.item} (${(offer.quantity * oVal).toFixed(2)} Ist)\n` + `Ask: ${askLines.join(' + ')}${total !== '--' ? ` = ${total} Ist` : ''}`; } function showTooltip(el, txt) { let tip = document.getElementById('rune-tooltip'); if (!tip) { tip = document.createElement('div'); tip.id = 'rune-tooltip'; document.body.appendChild(tip); } tip.textContent = txt; tip.style.display = 'block'; const r = el.getBoundingClientRect(), w = tip.offsetWidth; tip.style.top = `${window.scrollY + r.top}px`; tip.style.left = `${window.scrollX + r.left - w - 10}px`; } function hideTooltip() { const t = document.getElementById('rune-tooltip'); if (t) t.style.display = 'none'; } function injectPercentAndTooltip(off, group, prices, slug) { const container = group.element.querySelector('div:first-child'); if (!container || container.querySelector('.percent-injected')) return; const getVal = r => prices[slug]?.[r.item]?.ist_value ?? null; const oV = getVal(off); if (oV == null) return; container.style.position = 'relative'; const allAsk = group.items.every(r => getVal(r) != null); const span = document.createElement('span'); span.className = 'percent-injected'; span.style.left = '-60px'; span.textContent = allAsk ? (() => { const askTotal = group.items.reduce((s, r) => s + r.quantity * getVal(r), 0); const diff = ((off.quantity * oV - askTotal) / (off.quantity * oV)) * 100; return `${diff >= 0 ? '+' : ''}${Math.round(diff)}%`; })() : '(--)'; span.style.color = allAsk ? (off.quantity * oV - group.items.reduce((s, r) => s + r.quantity * getVal(r), 0) >= 0 ? 'rgb(0,200,0)' : 'rgb(220,80,80)') : 'gray'; container.appendChild(span); const tipText = buildTooltipText(off, group.items, prices, slug); span.addEventListener('mouseenter', () => showTooltip(span, tipText)); span.addEventListener('mouseleave', hideTooltip); } function injectAll(prices) { const slug = getServerSlug(); document.querySelectorAll('a.listing-name.selling-listing:not([data-injected])').forEach(a => { a.setAttribute('data-injected', 'true'); const cnt = a.closest('div[class*="sc-eqUAAy"]'); if (!cnt) return; const off = parseRune(a); if (!off || prices[slug]?.[off.item]?.ist_value == null) return; parseAskGroups(cnt).forEach(g => injectPercentAndTooltip(off, g, prices, slug)); }); } function fetchRunePrices(cb) { GM_xmlhttpRequest({ method: 'GET', url: PRICE_URL, onload(resp) { try { cb(JSON.parse(resp.responseText)); } catch (e) { console.error('Failed to parse rune prices', e); } }, onerror(err) { console.error('Failed to fetch rune prices', err); } }); } function setupRuneObserver() { if (runeObserver) runeObserver.disconnect(); runeObserver = new MutationObserver(() => { if (isRunePricingEnabled() && runePrices) { injectAll(runePrices); } }); runeObserver.observe(document.body, { childList: true, subtree: true }); if (isRunePricingEnabled() && runePrices) { if (document.readyState !== 'loading') { injectAll(runePrices); } else { document.addEventListener('DOMContentLoaded', () => injectAll(runePrices)); } } } function startRunePricing() { setRunePricingEnabled(true); if (!runePrices) { fetchRunePrices(prices => { runePrices = prices; setupRuneObserver(); }); } else { setupRuneObserver(); } } function stopRunePricing() { setRunePricingEnabled(false); runeObserver?.disconnect(); runeObserver = null; document.querySelectorAll('.percent-injected').forEach(e => e.remove()); document.querySelectorAll('[data-injected]').forEach(e => e.removeAttribute('data-injected')); hideTooltip(); } /*********************** * UI & Bookmarking * ***********************/ const font = document.createElement('link'); font.href = 'https://fonts.googleapis.com/css2?family=Alegreya+Sans&display=swap'; font.rel = 'stylesheet'; document.head.appendChild(font); const style = document.createElement('style'); style.textContent = ` .traderie-tools { position: fixed; top: 120px; left: 20px; width: 300px; background: #202224; color: #e4e6eb; border-radius: 20px; box-shadow: 0 0 10px rgba(0,0,0,0.3); font-family: 'Alegreya Sans', sans-serif; z-index: 9999; resize: both; overflow: auto; } .tools-header { background: #2a2b2e; padding: 10px 14px; border-radius: 20px 20px 0 0; cursor: move; display: flex; justify-content: space-between; align-items: center; font-size: 17px; font-weight: bold; user-select: none; } .tools-toggle { cursor: pointer; background: none !important; border: none; color: inherit; font-size: 18px; } .tools-toggle:hover { background: none !important; } .tools-content { padding: 12px 14px; display: none; } .tools-content.show { display: block; } .tab-buttons { display: flex; gap: 6px; margin-bottom: 10px; } .tab-buttons button { flex: 1; padding: 6px; border: none; border-radius: 12px; background: #2f3135; color: #e4e6eb; cursor: pointer; font-family: inherit; } .tab-buttons button.active { background: #444; font-weight: bold; } .tab-pane { display: none; } .tab-pane.active { display: block; } .tools-options { display: flex !important; flex-direction: column !important; gap: 6px !important; margin-bottom: 12px !important; align-items: flex-start !important; } .tools-options label { display: flex !important; align-items: center !important; gap: 6px !important; width: auto !important; text-align: left !important; font-size: 14px; margin: 0 !important; padding: 0 !important; } .tools-options input[type="checkbox"] { margin: 0 !important; padding: 0 !important; flex: 0 0 auto !important; width: auto !important; } .bookmark-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; margin-bottom: 6px; font-weight: normal; font-size: 14px; } .bookmark-header .arrow { transition: transform 0.2s ease; } .bookmark-header.expanded .arrow { transform: rotate(0deg); } .bookmark-header.collapsed .arrow { transform: rotate(-90deg); } .bookmark-section { display: none; flex-direction: column; gap: 6px; margin-left: 10px; } .bookmark-label { font-weight: bold; margin-top: 10px; margin-left: 10px; } .bookmark-item { display: flex; align-items: center; background: #2f3135; padding: 4px 10px; border-radius: 6px; font-size: 14px; justify-content: space-between; } .bookmark-item span.text { flex-grow: 1; cursor: pointer; margin-right: 10px; } .edit-btn, .delete-btn { color: #fff; font-size: 13px; margin-left: 6px; cursor: pointer; } .delete-btn { font-weight: bold; } .bookmark-edit-input { width: 100%; padding: 2px 6px; background: #444; color: #fff; border: none; border-radius: 4px; } .bookmark-name-input { width: 100%; padding: 6px; border-radius: 6px; background: #333; color: #fff; border: none; margin-bottom: 8px; display: none; } #rune-tooltip { position: absolute; background: #222; color: #fff; padding: 6px; border-radius: 4px; font-size: 12px; white-space: pre; z-index: 9999; max-width: 300px; display: none; } .percent-injected { position: absolute; top: 50%; transform: translateY(-50%); width: 50px; text-align: right; font-size: 14px; font-family: 'Alegreya Sans', sans-serif; white-space: nowrap; } .traderie-tools.collapsed { height: auto !important; overflow: hidden !important; resize: none !important; } .traderie-tools.collapsed .tools-content { display: none !important; } `; document.head.appendChild(style); const STORAGE_KEY_OPEN = 'traderieAppOpen'; const STORAGE_BOOKMARKS = 'traderieBookmarks'; const STORAGE_SIZE = 'traderieAppSize'; const STORAGE_POSITION = 'traderieAppPosition'; const STORAGE_BOOKMARKS_OPEN = 'traderieBookmarksOpen'; const panel = document.createElement('div'); panel.className = 'traderie-tools'; panel.innerHTML = ` <div class="tools-header" id="dragHandle"> <span>🔖 Traderie Tools</span> <button class="tools-toggle" id="expandBtn">➕</button> </div> <div class="tools-content" id="panelContent"> <div class="tab-buttons"> <button class="tab-btn active" data-tab="mainTab">Bookmarks</button> <button class="tab-btn" data-tab="optionsTab">Options</button> </div> <div id="mainTab" class="tab-pane active"> <div class="bookmark-header collapsed" id="bookmarkToggle"> <span> <svg class="arrow" stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" height="1em" width="1em"> <path d="M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z"></path> </svg> Bookmarks </span> <button id="saveSearchBtn" style="background: none; border: none; color: #e4e6eb; font-size: 14px; cursor: pointer;">+ Add to bookmarks</button> </div> <input id="bookmarkName" class="bookmark-name-input" placeholder="Name bookmark..."> <div id="bookmarkSection" class="bookmark-section"> <div class="bookmark-label">Listings</div> <div id="listingList"></div> <div class="bookmark-label">Searches</div> <div id="searchList"></div> </div> </div> <div id="optionsTab" class="tab-pane"> <div class="tools-options"> <label><input type="checkbox" id="cb1"> Remove Ads</label> <label><input type="checkbox" id="cb2"> Inject Rune Pricing</label> </div> </div> </div> `; document.body.appendChild(panel); const saveSize = () => { localStorage.setItem(STORAGE_SIZE, JSON.stringify({ width: panel.style.width, height: panel.style.height })); }; const loadSize = () => { const size = JSON.parse(localStorage.getItem(STORAGE_SIZE) || '{}'); if (size.width) panel.style.width = size.width; if (size.height) panel.style.height = size.height; }; const savePosition = () => { localStorage.setItem(STORAGE_POSITION, JSON.stringify({ left: panel.style.left, top: panel.style.top })); }; const loadPosition = () => { const pos = JSON.parse(localStorage.getItem(STORAGE_POSITION) || '{}'); if (pos.left) panel.style.left = pos.left; if (pos.top) panel.style.top = pos.top; }; const saveBookmarkToggle = (state) => { localStorage.setItem(STORAGE_BOOKMARKS_OPEN, JSON.stringify(state)); }; const loadBookmarkToggle = () => { return JSON.parse(localStorage.getItem(STORAGE_BOOKMARKS_OPEN) || 'false'); }; loadSize(); loadPosition(); const expandBtn = document.getElementById('expandBtn'); const content = document.getElementById('panelContent'); const saveState = () => localStorage.setItem(STORAGE_KEY_OPEN, content.classList.contains('show')); const loadState = () => localStorage.getItem(STORAGE_KEY_OPEN) === 'true'; if (loadState()) { content.classList.add('show'); expandBtn.textContent = '➖'; } expandBtn.onclick = () => { const isCollapsed = panel.classList.toggle('collapsed'); expandBtn.textContent = isCollapsed ? '➕' : '➖'; saveState(); }; document.querySelectorAll('.tab-btn').forEach(btn => { btn.onclick = () => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); btn.classList.add('active'); document.getElementById(btn.dataset.tab).classList.add('active'); }; }); const bookmarkToggle = document.getElementById('bookmarkToggle'); const bookmarkSection = document.getElementById('bookmarkSection'); bookmarkToggle.addEventListener('click', () => { const expanded = bookmarkToggle.classList.toggle('expanded'); bookmarkToggle.classList.toggle('collapsed', !expanded); bookmarkSection.style.display = expanded ? 'flex' : 'none'; saveBookmarkToggle(expanded); }); if (loadBookmarkToggle()) { bookmarkToggle.classList.add('expanded'); bookmarkToggle.classList.remove('collapsed'); bookmarkSection.style.display = 'flex'; } const getBookmarks = () => JSON.parse(localStorage.getItem(STORAGE_BOOKMARKS) || '[]'); const saveBookmarks = (bms) => localStorage.setItem(STORAGE_BOOKMARKS, JSON.stringify(bms)); const createEditButton = (callback) => { const btn = document.createElement('span'); btn.className = 'edit-btn'; btn.textContent = '✎'; btn.onclick = callback; return btn; }; const createDeleteButton = (callback) => { const btn = document.createElement('span'); btn.className = 'delete-btn'; btn.textContent = '🗑'; btn.onclick = callback; return btn; }; const renderBookmarks = () => { const bookmarks = getBookmarks(); const listings = bookmarks.filter(b => b.type === 'listing'); const searches = bookmarks.filter(b => b.type === 'search'); const listingList = document.getElementById('listingList'); const searchList = document.getElementById('searchList'); listingList.innerHTML = ''; searchList.innerHTML = ''; const makeItem = (b, idx, all) => { const item = document.createElement('div'); item.className = 'bookmark-item'; const text = document.createElement('span'); text.className = 'text'; text.textContent = b.name; text.onclick = () => window.location.href = b.url; const edit = createEditButton(() => { const input = document.createElement('input'); input.className = 'bookmark-edit-input'; input.value = b.name; input.onkeypress = (e) => { if (e.key === 'Enter') { all[idx].name = input.value.trim(); saveBookmarks(all); renderBookmarks(); } }; item.innerHTML = ''; item.appendChild(input); input.focus(); input.select(); }); const del = createDeleteButton(() => { const index = all.findIndex(x => x.name === b.name && x.url === b.url && x.type === b.type); if (index !== -1) { all.splice(index, 1); saveBookmarks(all); renderBookmarks(); } }); item.appendChild(text); item.appendChild(edit); item.appendChild(del); return item; }; listings.forEach((b, i) => listingList.appendChild(makeItem(b, i, bookmarks))); searches.forEach((b, i) => searchList.appendChild(makeItem(b, i + listings.length, bookmarks))); }; renderBookmarks(); const input = document.getElementById('bookmarkName'); const saveSearchBtn = document.getElementById('saveSearchBtn'); saveSearchBtn.onclick = () => { input.style.display = 'block'; input.value = ''; input.focus(); input.select(); }; input.onkeypress = (e) => { if (e.key === 'Enter') { const name = input.value.trim(); if (!name) return; const url = window.location.href; const isSearch = !url.includes('/listing'); const bookmarks = getBookmarks(); bookmarks.push({ type: isSearch ? 'search' : 'listing', name, url }); saveBookmarks(bookmarks); renderBookmarks(); input.style.display = 'none'; if (!bookmarkSection.style.display || bookmarkSection.style.display === 'none') { bookmarkToggle.classList.add('expanded'); bookmarkToggle.classList.remove('collapsed'); bookmarkSection.style.display = 'flex'; saveBookmarkToggle(true); } } }; // Adblock & Rune checkbox logic const adBox = document.getElementById('cb1'); const runeBox = document.getElementById('cb2'); adBox.checked = window.traderieAdblock.isEnabled(); runeBox.checked = isRunePricingEnabled(); if (adBox.checked) window.traderieAdblock.enable(); else window.traderieAdblock.disable(); if (runeBox.checked) startRunePricing(); adBox.addEventListener('change', () => { if (adBox.checked) window.traderieAdblock.enable(); else window.traderieAdblock.disable(); }); runeBox.addEventListener('change', () => { if (runeBox.checked) startRunePricing(); else stopRunePricing(); }); // Drag & resize persistence const dragHandle = document.getElementById('dragHandle'); let isDragging = false, offsetX = 0, offsetY = 0; dragHandle.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - panel.offsetLeft; offsetY = e.clientY - panel.offsetTop; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (isDragging) { panel.style.left = `${e.clientX - offsetX}px`; panel.style.top = `${e.clientY - offsetY}px`; } }); document.addEventListener('mouseup', () => { if (isDragging) savePosition(); isDragging = false; }); panel.addEventListener('mouseup', saveSize); })();