Papanads PF ADV menus (mobile draggable)

Plushie/flower helper anywhere. Counts API display + (inventory if available) + items page. Shows market total. Market update button. Shows 3 weakest for next set. Draggable tab launcher, position saved, mobile/PDA friendly.

// ==UserScript==
// @name         Papanads PF ADV menus (mobile draggable)
// @namespace    https://torn.com/
// @version      6.3
// @description  Plushie/flower helper anywhere. Counts API display + (inventory if available) + items page. Shows market total. Market update button. Shows 3 weakest for next set. Draggable tab launcher, position saved, mobile/PDA friendly.
// @author       you
// @license      MIT
// @match        https://www.torn.com/*
// @match        https://www.torn.com/page.php*
// @match        https://www.torn.com/item.php*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// ==/UserScript==

(function () {
  'use strict';
  console.log('[PF] userscript loaded');

  // =========================
  // CONFIG / STORAGE KEYS
  // =========================
  const API_STORAGE_KEY = 'pf_sets_api_key';
  const GOAL_PLUSH_KEY = 'pf_sets_goal_plush';
  const GOAL_FLOWER_KEY = 'pf_sets_goal_flower';
  const CACHE_STORAGE = 'pf_sets_cached_counts';
  const MARKET_CACHE_STORAGE = 'pf_sets_market_cache';
  const POS_X_KEY = 'pf_sets_pos_x';
  const POS_Y_KEY = 'pf_sets_pos_y';
  const REFRESH_MS = 30 * 1000;

  // =========================
  // KNOWN ITEM IDS
  // =========================
  const PLUSHIES = [
    { id: 187, name: 'Teddy Bear' },
    { id: 384, name: 'Camel' },
    { id: 273, name: 'Chamois' },
    { id: 258, name: 'Jaguar' },
    { id: 215, name: 'Kitten' },
    { id: 281, name: 'Lion' },
    { id: 269, name: 'Monkey' },
    { id: 266, name: 'Nessie' },
    { id: 274, name: 'Panda' },
    { id: 268, name: 'Red Fox' },
    { id: 186, name: 'Sheep' },
    { id: 618, name: 'Stingray' },
    { id: 261, name: 'Wolverine' },
  ];
  const FLOWERS = [
    { id: 260, name: 'Dahlia' },
    { id: 264, name: 'Orchid' },
    { id: 282, name: 'African Violet' },
    { id: 277, name: 'Cherry Blossom' },
    { id: 276, name: 'Peony' },
    { id: 271, name: 'Ceibo Flower' },
    { id: 272, name: 'Edelweiss' },
    { id: 263, name: 'Crocus' },
    { id: 267, name: 'Heather' },
    { id: 385, name: 'Tribulus Omanense' },
    { id: 617, name: 'Banana Orchid' },
  ];
  const PLUSH_ID_SET = new Set(PLUSHIES.map(p => p.id));
  const FLOWER_ID_SET = new Set(FLOWERS.map(f => f.id));

  // =========================
  // STATE
  // =========================
  let apiKey = GM_getValue(API_STORAGE_KEY, '') || '';
  let plushCounts = {};
  let flowerCounts = {};
  let lastApiError = '';
  let currentTab = 'plush';
  let panelOpen = false;
  let intervalsStarted = false;
  let marketData = loadMarketCache();
  let launcherEl = null;
  let panelEl = null;

  // =========================
  // INIT
  // =========================
  window.addEventListener('load', () => {
    createUI();
    loadCachedCounts();
    renderCurrent();

    if (!apiKey) {
      const entered = prompt('PF menus: enter your Torn API key (user:display, maybe user:inventory, torn:items for market):', '');
      if (entered) {
        apiKey = entered.trim();
        GM_setValue(API_STORAGE_KEY, apiKey);
      }
    }

    if (apiKey) {
      fetchUserItems();
    } else {
      // still try to read items page
      mergePageInventory(plushCounts, flowerCounts);
      renderCurrent();
    }

    startIntervals();
  });

  // =========================
  // PERIODIC FETCH
  // =========================
  function startIntervals() {
    if (intervalsStarted) return;
    intervalsStarted = true;
    setInterval(() => {
      if (apiKey) fetchUserItems();
      else {
        mergePageInventory(plushCounts, flowerCounts);
        renderCurrent();
      }
    }, REFRESH_MS);
  }

  // =========================
  // API FETCH
  // =========================
  function fetchUserItems() {
    if (!apiKey) return;

    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://api.torn.com/user/?selections=inventory,display&key=' + encodeURIComponent(apiKey),
      timeout: 10000,
      onload: function (res) {
        try {
          const data = JSON.parse(res.responseText);
          if (data.error) {
            lastApiError = `API error ${data.error.code}: ${data.error.error}`;
            renderCurrent();
            return;
          }

          const newPlush = {};
          const newFlower = {};

          // inventory
          const inv = data.inventory;
          if (Array.isArray(inv)) {
            inv.forEach(it => addToBuckets(it, newPlush, newFlower));
          } else if (inv && typeof inv === 'object') {
            Object.values(inv).forEach(it => addToBuckets(it, newPlush, newFlower));
          } else if (typeof inv === 'string') {
            if (inv.trim() === 'The inventory selection is no longer available') {
              lastApiError = 'Torn API: inventory selection is no longer available for this key.';
            } else {
              lastApiError = 'Inventory came back as text: ' + inv;
            }
          }

          // display
          const disp = data.display;
          if (Array.isArray(disp)) {
            disp.forEach(it => addToBuckets(it, newPlush, newFlower));
          } else if (disp && typeof disp === 'object') {
            Object.values(disp).forEach(it => addToBuckets(it, newPlush, newFlower));
          }

          // also merge page items (if on item page)
          mergePageInventory(newPlush, newFlower);

          plushCounts = newPlush;
          flowerCounts = newFlower;

          if (typeof inv !== 'string') {
            lastApiError = '';
          }

          saveCachedCounts();
          renderCurrent();
        } catch (e) {
          lastApiError = 'Could not read API response.';
          renderCurrent();
        }
      },
      onerror: () => {
        lastApiError = 'Request failed.';
        renderCurrent();
      },
      ontimeout: () => {
        lastApiError = 'API request timed out.';
        renderCurrent();
      }
    });
  }

  // =========================
  // MERGE PAGE INVENTORY (ITEMS PAGE)
  // =========================
  function mergePageInventory(plushBucket, flowerBucket) {
    const isItemsPage =
      location.pathname.includes('/item.php') ||
      document.querySelector('.items-cont') ||
      document.querySelector('[class*="inventoryWrapper"]');

    if (!isItemsPage) return;

    const possibleItemNodes = document.querySelectorAll('[data-item], [data-id], li.item, div.item');
    if (!possibleItemNodes.length) return;

    possibleItemNodes.forEach(node => {
      let id = node.getAttribute('data-item') || node.getAttribute('data-id');
      if (!id && node.dataset) {
        id = node.dataset.item || node.dataset.id;
      }
      if (!id) return;
      const numId = Number(id);
      if (!numId) return;

      let qty = 1;
      const qtyNode = node.querySelector('[class*="amount"], .qty, .quantity, [data-amount]');
      if (qtyNode) {
        const txt = qtyNode.textContent || qtyNode.getAttribute('data-amount') || '1';
        const m = txt.match(/\d+/);
        if (m) qty = Number(m[0]);
      }

      if (PLUSH_ID_SET.has(numId)) {
        plushBucket[numId] = (plushBucket[numId] || 0) + qty;
      } else if (FLOWER_ID_SET.has(numId)) {
        flowerBucket[numId] = (flowerBucket[numId] || 0) + qty;
      }
    });
  }

  // =========================
  // MARKET UPDATE
  // =========================
  function updateMarketData() {
    if (!apiKey) {
      lastApiError = 'Set API key first.';
      renderCurrent();
      return;
    }
    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://api.torn.com/torn/?selections=items&key=' + encodeURIComponent(apiKey),
      timeout: 10000,
      onload: function (res) {
        try {
          const data = JSON.parse(res.responseText);
          if (data.error) {
            lastApiError = `Market error ${data.error.code}: ${data.error.error}`;
            renderCurrent();
            return;
          }
          const items = data.items || {};
          const newMarket = {};
          const wanted = new Set([...PLUSHIES.map(p => p.id), ...FLOWERS.map(f => f.id)]);
          Object.keys(items).forEach(idStr => {
            const id = Number(idStr);
            if (!wanted.has(id)) return;
            const item = items[idStr];
            newMarket[id] = {
              market_value: item.market_value || 0,
              name: item.name || ''
            };
          });
          marketData = newMarket;
          saveMarketCache();
          lastApiError = '';
          renderCurrent();
        } catch (e) {
          lastApiError = 'Could not read market response.';
          renderCurrent();
        }
      },
      onerror: () => {
        lastApiError = 'Market request failed.';
        renderCurrent();
      },
      ontimeout: () => {
        lastApiError = 'Market request timed out.';
        renderCurrent();
      }
    });
  }

  // =========================
  // HELPERS
  // =========================
  function addToBuckets(it, plushBucket, flowerBucket) {
    if (!it) return;
    const id = Number(it.ID || it.id);
    const qty = Number(it.quantity || it.qty || 1);
    if (!id || !qty) return;
    if (PLUSH_ID_SET.has(id)) {
      plushBucket[id] = (plushBucket[id] || 0) + qty;
    } else if (FLOWER_ID_SET.has(id)) {
      flowerBucket[id] = (flowerBucket[id] || 0) + qty;
    }
  }

  function calcMarketTotal(bucket) {
    let total = 0;
    Object.keys(bucket).forEach(idStr => {
      const id = Number(idStr);
      const qty = bucket[id] || 0;
      const m = marketData[id];
      if (m && m.market_value) {
        total += m.market_value * qty;
      }
    });
    return total;
  }

  // =========================
  // CACHE
  // =========================
  function saveCachedCounts() {
    GM_setValue(CACHE_STORAGE, JSON.stringify({ plushCounts, flowerCounts, ts: Date.now() }));
  }
  function loadCachedCounts() {
    const raw = GM_getValue(CACHE_STORAGE, '');
    if (!raw) return;
    try {
      const data = JSON.parse(raw);
      if (data.plushCounts) plushCounts = data.plushCounts;
      if (data.flowerCounts) flowerCounts = data.flowerCounts;
    } catch (e) {}
  }
  function saveMarketCache() {
    GM_setValue(MARKET_CACHE_STORAGE, JSON.stringify(marketData));
  }
  function loadMarketCache() {
    const raw = GM_getValue(MARKET_CACHE_STORAGE, '');
    if (!raw) return {};
    try {
      return JSON.parse(raw);
    } catch (e) {
      return {};
    }
  }

  // =========================
  // RENDER
  // =========================
  function renderCurrent() {
    if (currentTab === 'plush') renderPlush();
    else if (currentTab === 'flower') renderFlower();
    else renderSettings();
    updatePanelPosition();
  }

  function renderPlush() {
    const header = document.getElementById('pf-header-title');
    const content = document.getElementById('pf-content');
    if (!header || !content) return;

    header.textContent = 'Plushie Sets';

    const counts = PLUSHIES.map(p => plushCounts[p.id] || 0);
    const sets = counts.length ? Math.min(...counts) : 0;
    const target = sets + 1;

    const weakest = PLUSHIES
      .map(p => ({
        id: p.id,
        name: p.name + ' Plushie',
        have: plushCounts[p.id] || 0
      }))
      .sort((a, b) => a.have - b.have)
      .slice(0, 3);

    const marketTotal = calcMarketTotal(plushCounts);

    const savedGoalStr = GM_getValue(GOAL_PLUSH_KEY, '');
    const savedGoal = savedGoalStr ? Number(savedGoalStr) : null;
    const goalReached = savedGoal !== null && sets >= savedGoal;

    let html = '';
    html += `<div class="pf-market-total">Market value (inv + display): $${marketTotal.toLocaleString()}</div>`;
    html += `<div class="pf-top pf-top-plush">Plushies tracked: ${counts.reduce((a, b) => a + b, 0)}</div>`;
    html += `<div class="pf-main-sets-line">
      <span>You: ${sets}</span>
      <span class="pf-goal-inline ${goalReached ? 'pf-goal-inline-green' : 'pf-goal-inline-red'}">Goal: ${savedGoal !== null ? savedGoal : '–'}</span>
    </div>`;
    html += `<div class="pf-goal-toggle" id="pf-goal-toggle">Goal settings ▼</div>
      <div class="pf-goal-panel" id="pf-goal-panel" style="display:none;">
        <label>Goal sets:</label>
        <input type="number" id="pf-goal-input" min="0" value="${savedGoal !== null ? savedGoal : ''}">
        <button id="pf-goal-save" class="pf-goal-btn">Save</button>
      </div>`;
    html += `<div class="pf-section-title">Weakest for next set (aim for ≥ ${target})</div>`;

    if (weakest.length) {
      html += `<div class="pf-list">` + weakest.map(item => {
        const market = marketData[item.id];
        const priceStr = market && market.market_value ? ` <span class="pf-market">m$${market.market_value.toLocaleString()}</span>` : '';
        return `
          <div class="pf-row">
            <span class="pf-name">${item.name}${priceStr}</span>
            <span class="pf-right">you have ${item.have}</span>
          </div>
        `;
      }).join('') + `</div>`;
    } else {
      html += `<div class="pf-empty">Already enough for next set.</div>`;
    }

    if (lastApiError) {
      html += `<div class="pf-error">${lastApiError}</div>`;
    }

    content.innerHTML = html;

    const tog = document.getElementById('pf-goal-toggle');
    const pnl = document.getElementById('pf-goal-panel');
    const inp = document.getElementById('pf-goal-input');
    const saveBtn = document.getElementById('pf-goal-save');
    if (tog && pnl) {
      tog.addEventListener('click', () => {
        const vis = pnl.style.display !== 'none';
        pnl.style.display = vis ? 'none' : 'block';
        tog.textContent = vis ? 'Goal settings ▼' : 'Goal settings ▲';
      });
    }
    if (saveBtn && inp) {
      saveBtn.addEventListener('click', () => {
        GM_setValue(GOAL_PLUSH_KEY, inp.value);
        renderPlush();
      });
    }

    panelEl.classList.remove('pf-flower-theme');
    panelEl.classList.add('pf-plush-theme');
  }

  function renderFlower() {
    const header = document.getElementById('pf-header-title');
    const content = document.getElementById('pf-content');
    if (!header || !content) return;

    header.textContent = 'Flower Sets';

    const counts = FLOWERS.map(f => flowerCounts[f.id] || 0);
    const sets = counts.length ? Math.min(...counts) : 0;
    const target = sets + 1;

    const weakest = FLOWERS
      .map(f => ({
        id: f.id,
        name: f.name,
        have: flowerCounts[f.id] || 0
      }))
      .sort((a, b) => a.have - b.have)
      .slice(0, 3);

    const marketTotal = calcMarketTotal(flowerCounts);

    const savedGoalStr = GM_getValue(GOAL_FLOWER_KEY, '');
    const savedGoal = savedGoalStr ? Number(savedGoalStr) : null;
    const goalReached = savedGoal !== null && sets >= savedGoal;

    let html = '';
    html += `<div class="pf-market-total">Market value (inv + display): $${marketTotal.toLocaleString()}</div>`;
    html += `<div class="pf-top pf-top-flower">Flowers tracked: ${counts.reduce((a, b) => a + b, 0)}</div>`;
    html += `<div class="pf-main-sets-line">
      <span>You: ${sets}</span>
      <span class="pf-goal-inline ${goalReached ? 'pf-goal-inline-green' : 'pf-goal-inline-red'}">Goal: ${savedGoal !== null ? savedGoal : '–'}</span>
    </div>`;
    html += `<div class="pf-goal-toggle" id="pf-goal-toggle">Goal settings ▼</div>
      <div class="pf-goal-panel" id="pf-goal-panel" style="display:none;">
        <label>Goal sets:</label>
        <input type="number" id="pf-goal-input" min="0" value="${savedGoal !== null ? savedGoal : ''}">
        <button id="pf-goal-save" class="pf-goal-btn">Save</button>
      </div>`;
    html += `<div class="pf-section-title">Weakest for next set (aim for ≥ ${target})</div>`;

    if (weakest.length) {
      html += `<div class="pf-list">` + weakest.map(item => {
        const market = marketData[item.id];
        const priceStr = market && market.market_value ? ` <span class="pf-market">m$${market.market_value.toLocaleString()}</span>` : '';
        return `
          <div class="pf-row">
            <span class="pf-name">${item.name}${priceStr}</span>
            <span class="pf-right">you have ${item.have}</span>
          </div>
        `;
      }).join('') + `</div>`;
    } else {
      html += `<div class="pf-empty">Already enough for next set.</div>`;
    }

    if (lastApiError) {
      html += `<div class="pf-error">${lastApiError}</div>`;
    }

    content.innerHTML = html;

    const tog = document.getElementById('pf-goal-toggle');
    const pnl = document.getElementById('pf-goal-panel');
    const inp = document.getElementById('pf-goal-input');
    const saveBtn = document.getElementById('pf-goal-save');
    if (tog && pnl) {
      tog.addEventListener('click', () => {
        const vis = pnl.style.display !== 'none';
        pnl.style.display = vis ? 'none' : 'block';
        tog.textContent = vis ? 'Goal settings ▼' : 'Goal settings ▲';
      });
    }
    if (saveBtn && inp) {
      saveBtn.addEventListener('click', () => {
        GM_setValue(GOAL_FLOWER_KEY, inp.value);
        renderFlower();
      });
    }

    panelEl.classList.remove('pf-plush-theme');
    panelEl.classList.add('pf-flower-theme');
  }

  function renderSettings() {
    const header = document.getElementById('pf-header-title');
    const content = document.getElementById('pf-content');
    if (!header || !content) return;

    header.textContent = 'PF ADV settings';
    const masked = apiKey ? mask(apiKey) : '(none)';

    content.innerHTML = `
      <div class="pf-settings-block">
        <div class="pf-settings-label">Saved API key:</div>
        <div class="pf-settings-value">${masked}</div>
        <input type="text" id="pf-api-input" placeholder="New API key..." style="width:100%;margin-top:6px;">
        <div class="pf-settings-btns">
          <button id="pf-api-save">Save key</button>
          <button id="pf-api-clear">Clear</button>
          <button id="pf-api-refresh">Refresh items</button>
          <button id="pf-market-refresh">Market update</button>
        </div>
        <div class="pf-settings-note">Tip: you can drag the tab buttons anywhere (mobile/PDA safe).</div>
        ${lastApiError ? `<div class="pf-settings-note" style="color:#ff6666;">${lastApiError}</div>` : ''}
      </div>
    `;

    const inpt = document.getElementById('pf-api-input');
    const save = document.getElementById('pf-api-save');
    const clr = document.getElementById('pf-api-clear');
    const ref = document.getElementById('pf-api-refresh');
    const mref = document.getElementById('pf-market-refresh');

    if (save && inpt) {
      save.addEventListener('click', () => {
        const v = inpt.value.trim();
        if (v) {
          apiKey = v;
          GM_setValue(API_STORAGE_KEY, v);
          fetchUserItems();
        }
      });
    }
    if (clr) {
      clr.addEventListener('click', () => {
        apiKey = '';
        GM_setValue(API_STORAGE_KEY, '');
        lastApiError = 'API key cleared.';
        renderSettings();
      });
    }
    if (ref) {
      ref.addEventListener('click', () => {
        if (apiKey) fetchUserItems();
        else {
          mergePageInventory(plushCounts, flowerCounts);
          renderCurrent();
        }
      });
    }
    if (mref) {
      mref.addEventListener('click', () => {
        updateMarketData();
      });
    }

    panelEl.classList.remove('pf-plush-theme', 'pf-flower-theme');
  }

  function mask(k) {
    if (k.length <= 6) return '***';
    return k.slice(0, 3) + '...' + k.slice(-3);
  }

  // =========================
  // UI / DRAGGABLE LAUNCHER
  // =========================
  function createUI() {
    const wrap = document.createElement('div');
    wrap.id = 'pf-wrap';
    document.body.appendChild(wrap);
    launcherEl = wrap;

    const savedX = GM_getValue(POS_X_KEY, 20);
    const savedY = GM_getValue(POS_Y_KEY, 20);
    wrap.style.left = savedX + 'px';
    wrap.style.top = savedY + 'px';

    const tabPlush = document.createElement('div');
    tabPlush.id = 'pf-tab-plush';
    tabPlush.className = 'pf-tab pf-tab-active';
    tabPlush.textContent = '🧸';
    wrap.appendChild(tabPlush);

    const tabFlower = document.createElement('div');
    tabFlower.id = 'pf-tab-flower';
    tabFlower.className = 'pf-tab';
    tabFlower.textContent = '🌸';
    wrap.appendChild(tabFlower);

    const tabSettings = document.createElement('div');
    tabSettings.id = 'pf-tab-settings';
    tabSettings.className = 'pf-tab pf-tab-settings';
    tabSettings.textContent = '⚙️';
    wrap.appendChild(tabSettings);

    const panel = document.createElement('div');
    panel.id = 'pf-panel';
    panel.innerHTML = `
      <div id="pf-panel-inner">
        <div class="pf-header" id="pf-header-title">
          Plushie Sets
          <span id="pf-close-btn">✖</span>
        </div>
        <div id="pf-content">Loading…</div>
      </div>
    `;
    document.body.appendChild(panel);
    panelEl = panel;

    tabPlush.addEventListener('click', () => {
      currentTab = 'plush';
      renderPlush();
      togglePanel('plush');
    });
    tabFlower.addEventListener('click', () => {
      currentTab = 'flower';
      renderFlower();
      togglePanel('flower');
    });
    tabSettings.addEventListener('click', () => {
      currentTab = 'settings';
      renderSettings();
      togglePanel('settings');
    });

    panel.querySelector('#pf-close-btn').addEventListener('click', () => {
      panel.classList.remove('pf-open');
      panelOpen = false;
    });

    makeDraggable(wrap);
    addStyles();
    updatePanelPosition();
  }

  function makeDraggable(el) {
    let isDown = false;
    let startX = 0;
    let startY = 0;
    let origX = 0;
    let origY = 0;

    const startDrag = (clientX, clientY) => {
      isDown = true;
      startX = clientX;
      startY = clientY;
      const rect = el.getBoundingClientRect();
      origX = rect.left;
      origY = rect.top;
    };

    const moveDrag = (clientX, clientY) => {
      if (!isDown) return;
      const dx = clientX - startX;
      const dy = clientY - startY;
      const newX = origX + dx;
      const newY = origY + dy;
      el.style.left = newX + 'px';
      el.style.top = newY + 'px';
      updatePanelPosition();
    };

    const endDrag = () => {
      if (!isDown) return;
      isDown = false;
      const rect = el.getBoundingClientRect();
      GM_setValue(POS_X_KEY, Math.round(rect.left));
      GM_setValue(POS_Y_KEY, Math.round(rect.top));
    };

    el.addEventListener('mousedown', e => {
      e.preventDefault();
      startDrag(e.clientX, e.clientY);
    });
    document.addEventListener('mousemove', e => moveDrag(e.clientX, e.clientY));
    document.addEventListener('mouseup', endDrag);

    el.addEventListener('touchstart', e => {
      const t = e.touches[0];
      startDrag(t.clientX, t.clientY);
    }, { passive: true });
    document.addEventListener('touchmove', e => {
      if (!isDown) return;
      const t = e.touches[0];
      moveDrag(t.clientX, t.clientY);
    }, { passive: true });
    document.addEventListener('touchend', endDrag);
  }

  function togglePanel(tabName) {
    if (!panelEl) return;
    if (panelOpen && currentTab === tabName) {
      panelEl.classList.remove('pf-open');
      panelOpen = false;
    } else {
      setActiveTab(tabName);
      updatePanelPosition();
      panelEl.classList.add('pf-open');
      panelOpen = true;
    }
  }

  function setActiveTab(tab) {
    const p = document.getElementById('pf-tab-plush');
    const f = document.getElementById('pf-tab-flower');
    const s = document.getElementById('pf-tab-settings');
    p && p.classList.remove('pf-tab-active');
    f && f.classList.remove('pf-tab-active');
    s && s.classList.remove('pf-tab-active');
    if (tab === 'plush' && p) p.classList.add('pf-tab-active');
    else if (tab === 'flower' && f) f.classList.add('pf-tab-active');
    else if (tab === 'settings' && s) s.classList.add('pf-tab-active');
  }

  function updatePanelPosition() {
    if (!launcherEl || !panelEl) return;
    const rect = launcherEl.getBoundingClientRect();
    const panelWidth = 280;
    const margin = 10;
    let left = rect.right + margin;
    let top = rect.top;

    if (left + panelWidth > window.innerWidth) {
      left = rect.left - panelWidth - margin;
    }
    if (left < 0) left = margin;
    if (top + panelEl.offsetHeight > window.innerHeight) {
      top = window.innerHeight - panelEl.offsetHeight - margin;
    }
    if (top < margin) top = margin;

    panelEl.style.left = left + 'px';
    panelEl.style.top = top + 'px';
  }

  // =========================
  // STYLES
  // =========================
  function addStyles() {
    const css = `
      @keyframes pfGlowGreen {
        0% { box-shadow: 0 0 0 rgba(0,255,0,0); }
        50% { box-shadow: 0 0 12px rgba(0,255,0,0.9); }
        100% { box-shadow: 0 0 0 rgba(0,255,0,0); }
      }
      @keyframes pfGlowYellow {
        0% { box-shadow: 0 0 0 rgba(255,255,0,0); }
        50% { box-shadow: 0 0 12px rgba(255,255,0,1); }
        100% { box-shadow: 0 0 0 rgba(255,255,0,0); }
      }

      #pf-wrap {
        position: fixed;
        display: flex;
        flex-direction: column;
        gap: 6px;
        z-index: 999999;
        bottom: 20px;
        left: 20px;
        touch-action: none;
      }
      .pf-tab {
        width: 52px;
        height: 52px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 10px;
        cursor: grab;
        font-size: 26px;
        background: rgba(0,0,0,0.55);
        transition: transform .15s;
        backdrop-filter: blur(4px);
      }
      .pf-tab:active {
        cursor: grabbing;
      }
      #pf-tab-plush {
        border: 1px solid rgba(0,255,0,0.7);
        animation: pfGlowGreen 3s ease-in-out infinite;
      }
      #pf-tab-flower {
        border: 1px solid rgba(255,255,0,0.7);
        animation: pfGlowYellow 3.2s ease-in-out infinite;
      }
      .pf-tab-settings {
        border: 1px solid rgba(255,255,255,0.35);
        font-size: 22px;
      }
      .pf-tab-active {
        transform: scale(1.02);
      }

      #pf-panel {
        position: fixed;
        width: 280px;
        max-height: 65vh;
        overflow-y: auto;
        border-radius: 12px;
        transform: translateX(-360px);
        opacity: 0;
        pointer-events: none;
        transition: opacity .2s ease-out;
        z-index: 999998;
      }
      #pf-panel.pf-open {
        transform: translateX(0);
        opacity: 1;
        pointer-events: auto;
      }

      .pf-header {
        padding: 9px 12px;
        font-weight: bold;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      #pf-content {
        padding: 8px 10px 10px 10px;
        font-size: 12px;
      }
      .pf-top {
        font-weight: bold;
        border-radius: 6px;
        padding: 4px 6px;
        margin-bottom: 6px;
      }
      .pf-main-sets-line {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 6px;
      }
      .pf-goal-inline {
        font-weight: bold;
      }
      .pf-goal-inline-red {
        color: #ff5c5c;
      }
      .pf-goal-inline-green {
        color: #6dff6d;
      }
      .pf-goal-toggle {
        font-size: 11px;
        cursor: pointer;
        opacity: .85;
        margin-bottom: 4px;
      }
      .pf-goal-panel {
        background: rgba(0,0,0,0.15);
        border: 1px solid rgba(255,255,255,0.05);
        border-radius: 4px;
        padding: 4px;
        margin-bottom: 6px;
      }
      .pf-goal-panel input {
        width: 70px;
        margin-right: 6px;
      }
      .pf-goal-btn {
        background: rgba(255,255,255,0.12);
        border: 1px solid rgba(255,255,255,0.2);
        border-radius: 4px;
        padding: 2px 6px;
        font-size: 11px;
        cursor: pointer;
      }
      .pf-section-title {
        font-weight: bold;
        margin: 6px 0 4px 0;
        font-size: 12px;
      }
      .pf-list {
        display: flex;
        flex-direction: column;
        gap: 3px;
      }
      .pf-row {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 2px 4px;
        border-radius: 4px;
        background: rgba(255,255,255,0.03);
      }
      .pf-name {
        flex: 1;
      }
      .pf-right {
        font-size: 11px;
        font-weight: bold;
      }
      .pf-empty {
        opacity: .7;
        font-style: italic;
      }
      .pf-error {
        margin-top: 6px;
        color: #ff6666;
        font-size: 11px;
      }
      .pf-market {
        font-size: 10px;
        opacity: .7;
        margin-left: 4px;
      }
      .pf-market-total {
        font-size: 11px;
        margin-bottom: 4px;
        opacity: .8;
      }

      .pf-plush-theme {
        background: rgba(0,0,0,0.7);
        border: 1px solid rgba(0,255,0,0.4);
        color: #bfff00;
      }
      .pf-plush-theme .pf-top-plush {
        background: rgba(0,255,0,0.08);
        border: 1px solid rgba(0,255,0,0.25);
      }
      .pf-flower-theme {
        background: rgba(0,0,0,0.7);
        border: 1px solid rgba(255,255,0,0.4);
        color: #ffe066;
      }
      .pf-flower-theme .pf-top-flower {
        background: rgba(255,255,0,0.05);
        border: 1px solid rgba(255,255,0,0.25);
      }

      .pf-settings-block {
        background: rgba(255,255,255,0.03);
        border: 1px solid rgba(255,255,255,0.06);
        border-radius: 6px;
        padding: 6px;
        font-size: 12px;
      }
      .pf-settings-label {
        font-weight: bold;
        margin-bottom: 2px;
      }
      .pf-settings-value {
        margin-bottom: 4px;
      }
      .pf-settings-note {
        font-size: 11px;
        opacity: .6;
        margin-top: 6px;
      }
      .pf-settings-btns {
        display: flex;
        gap: 6px;
        margin-top: 6px;
        flex-wrap: wrap;
      }
      .pf-settings-btns button {
        background: rgba(255,255,255,0.12);
        border: 1px solid rgba(255,255,255,0.25);
        border-radius: 4px;
        cursor: pointer;
        font-size: 11px;
        padding: 3px 6px;
      }

      @media (max-width: 768px) {
        #pf-wrap {
          gap: 5px;
        }
        .pf-tab {
          width: 50px;
          height: 50px;
        }
        #pf-panel {
          max-height: 60vh;
        }
      }
    `;
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
  }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址