Traderie Tools

A browsing companion for traderie.com with bookmarking, adblock, and Diablo 2 price history

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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);
})();