🌺 🐫 YATA Travel Stock — Flowers, Plushies, Xanax (Above PDA)

Shows foreign stock (flowers, plushies, and Xanax in SA) from YATA export. Refreshes every 20s. Above PDA.

От 11.10.2025. Виж последната версия.

// ==UserScript==
// @name         🌺 🐫 YATA Travel Stock — Flowers, Plushies, Xanax (Above PDA)
// @namespace    http://tampermonkey.net/
// @version      1.2.0
// @description  Shows foreign stock (flowers, plushies, and Xanax in SA) from YATA export. Refreshes every 20s. Above PDA.
// @author       Nova
// @match        https://www.torn.com/page.php?sid=travel*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      yata.yt
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';
  if (!/page\.php\?sid=travel/.test(location.href)) return;

  const YATA_URL = 'https://yata.yt/api/v1/travel/export/';
  const POLL_MS = 20 * 1000;

  const FLOWERS = [
    "Dahlia", "Orchid", "African Violet", "Cherry Blossom",
    "Peony", "Ceibo Flower", "Edelweiss", "Crocus",
    "Heather", "Tribulus Omanense", "Banana Orchid"
  ];

  const PLUSHIES = [
    "Sheep Plushie", "Teddy Bear Plushie", "Kitten Plushie",
    "Jaguar Plushie", "Wolverine Plushie", "Nessie Plushie",
    "Red Fox Plushie", "Monkey Plushie", "Chamois Plushie",
    "Panda Plushie", "Lion Plushie", "Camel Plushie",
    "Stingray Plushie"
  ];

  const TRACKED = new Set([...FLOWERS, ...PLUSHIES, "Xanax"]);

  const COUNTRY_NAMES = {
    mex: 'Mexico',
    cay: 'Cayman Islands',
    can: 'Canada',
    haw: 'Hawaii',
    uni: 'United Kingdom',
    arg: 'Argentina',
    swi: 'Switzerland',
    jap: 'Japan',
    chi: 'China',
    uae: 'UAE',
    sou: 'South Africa'
  };

  function getPDANavHeight() {
    const nav = document.querySelector('#pda-nav') || document.querySelector('.pda');
    return nav ? nav.offsetHeight : 40;
  }

  GM_addStyle(`
    #yataStockPanel {
      position: fixed;
      top: ${getPDANavHeight()}px;
      left: 18px;
      width: 320px;
      background: #080808;
      color: #e9eef6;
      font-family: "DejaVu Sans Mono", monospace;
      font-size: 11px;
      border: 1px solid #333;
      border-radius: 6px;
      z-index: 999999;
      box-shadow: 0 6px 18px rgba(0,0,0,0.6);
      max-height: 70vh;
      overflow-y: auto;
      line-height: 1.1;
      padding-bottom:6px;
    }
    #yataHeader {
      background:#101010;
      padding:6px 8px;
      cursor:pointer;
      font-weight:700;
      font-size:12px;
      border-bottom:1px solid #2b2b2b;
      user-select:none;
    }
    #yataContent { padding:8px; display:block; }
    .yata-controls { margin-bottom:8px; }
    .yata-controls button {
      margin:2px 6px 6px 0;
      font-size:11px;
      padding:4px 8px;
      background:#121212;
      color:#e9eef6;
      border:1px solid #2b2b2b;
      border-radius:4px;
      cursor:pointer;
    }
    .yata-controls button:hover { background:#1b1b1b; }
    #yataStatus { font-size:11px; color:#bdbdbd; margin-bottom:8px; }
    .country-block { margin-bottom:8px; border-top:1px dashed #222; padding-top:6px; }
    .country-title { font-weight:700; margin-bottom:4px; display:flex; justify-content:space-between; align-items:center; }
    .country-title .ct-left { font-size:12px; }
    .country-upd { font-size:10px; color:#9ea6b3; }
    .item-row { display:flex; justify-content:space-between; gap:8px; padding:2px 0; align-items:center; }
    .item-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; }
    .qty { width:64px; text-align:right; font-weight:700; }
    .cost { width:56px; text-align:right; color:#aeb7c4; font-size:11px; }
    .status-dot { width:10px; height:10px; border-radius:50%; display:inline-block; margin-right:6px; vertical-align:middle; }
    .dot-green { background:#00c853; }
    .dot-yellow { background:#ffb300; }
    .dot-red { background:#ff1744; }
    .small-note { font-size:11px; color:#9ea6b3; margin-top:6px; }
  `);

  const panel = document.createElement('div');
  panel.id = 'yataStockPanel';
  panel.innerHTML = `
    <div id="yataHeader">▶ 🌺 🐫 YATA Stock (Flowers & Plushies)</div>
    <div id="yataContent">
      <div class="yata-controls">
        <button id="yata_refresh">Refresh</button>
        <button id="yata_toggle_all">Collapse</button>
      </div>
      <div id="yataStatus">Loading...</div>
      <div id="yataList"></div>
      <div class="small-note">Data from yata.yt · flowers, plushies, and Xanax (SA) only · refresh 20s</div>
    </div>
  `;
  document.body.appendChild(panel);

  const header = panel.querySelector('#yataHeader');
  const content = panel.querySelector('#yataContent');
  const statusEl = panel.querySelector('#yataStatus');
  const listEl = panel.querySelector('#yataList');
  const btnRefresh = panel.querySelector('#yata_refresh');
  const btnToggle = panel.querySelector('#yata_toggle_all');

  let collapsed = GM_getValue('yata_collapsed', false);
  function updateCollapseUI() {
    content.style.display = collapsed ? 'none' : 'block';
    header.textContent = (collapsed ? '▶' : '▼') + ' 🌺 🐫 YATA Stock (Flowers & Plushies)';
    btnToggle.textContent = collapsed ? 'Expand' : 'Collapse';
    GM_setValue('yata_collapsed', collapsed);
  }
  updateCollapseUI();

  header.addEventListener('click', () => { collapsed = !collapsed; updateCollapseUI(); });
  btnToggle.addEventListener('click', () => { collapsed = !collapsed; updateCollapseUI(); });
  btnRefresh.addEventListener('click', () => fetchAndRender(true));

  function qtyClass(q) {
    if (q <= 0) return 'dot-red';
    if (q <= 10) return 'dot-yellow';
    return 'dot-green';
  }

  function gmFetchJson(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        responseType: 'json',
        onload: (res) => {
          let data = res.response;
          if (!data && res.responseText) {
            try { data = JSON.parse(res.responseText); } catch(e){}
          }
          if (!data) reject(new Error('No JSON'));
          else resolve(data);
        },
        onerror: reject
      });
    });
  }

  function renderExport(data) {
    if (!data || !data.stocks) {
      statusEl.textContent = 'No stock data.';
      listEl.innerHTML = '';
      return;
    }

    const ts = data.timestamp ? new Date(data.timestamp * 1000) : null;
    statusEl.textContent = ts ? `Last payload: ${ts.toUTCString()}` : 'Live data';

    let html = '';
    Object.keys(data.stocks).forEach(code => {
      const c = data.stocks[code];
      if (!c) return;
      const name = COUNTRY_NAMES[code] || code.toUpperCase();
      const upd = c.update ? new Date(c.update * 1000) : null;

      const items = Array.isArray(c.stocks) ? c.stocks.filter(it => {
        if (TRACKED.has(it.name)) {
          if (it.name === "Xanax") return code === "sou"; // only SA
          return true;
        }
        return false;
      }) : [];

      if (!items.length) return;

      html += `<div class="country-block" data-country="${code}">
        <div class="country-title">
          <div class="ct-left">${name} <span class="country-upd">${upd ? upd.toUTCString() : ''}</span></div>
          <div class="ct-right">${code}</div>
        </div>`;

      items.forEach(it => {
        const q = Number(it.quantity ?? 0);
        const cost = it.cost != null ? Number(it.cost) : null;
        const dot = `<span class="status-dot ${qtyClass(q)}"></span>`;
        html += `<div class="item-row">
          <div class="item-name">${dot}${escapeHtml(it.name)}</div>
          <div class="cost">${cost != null ? formatNumber(cost) : '-'}</div>
          <div class="qty">${q}</div>
        </div>`;
      });
      html += `</div>`;
    });

    listEl.innerHTML = html || '<div style="color:#999;">No tracked items found.</div>';
  }

  function escapeHtml(s) {
    return s ? s.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m])) : '';
  }

  function formatNumber(n) {
    return n.toLocaleString('en-US');
  }

  let pollHandle = null;
  let lastPayloadTimestamp = 0;

  async function fetchAndRender(force=false) {
    try {
      statusEl.textContent = 'Fetching YATA export...';
      const data = await gmFetchJson(YATA_URL);
      if (!force && data.timestamp === lastPayloadTimestamp) return;
      lastPayloadTimestamp = data.timestamp;
      renderExport(data);
    } catch (err) {
      statusEl.textContent = 'Fetch error: ' + (err.message || err);
      listEl.innerHTML = '';
    }
  }

  function startPolling() {
    if (pollHandle) return;
    fetchAndRender(true);
    pollHandle = setInterval(() => fetchAndRender(false), POLL_MS);
  }

  startPolling();
  window.addEventListener('beforeunload', () => clearInterval(pollHandle));

})();