Torn - AFK Market

Analyze your listings while you are unable to.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Torn - AFK Market
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Analyze your listings while you are unable to.
// @author       Upsilon [3212478]
// @license      ISC
// @match        https://www.torn.com/profiles.php?XID=*
// @connect      api.torn.com
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
    'use strict';

    class TornItemMarketAnalyzer {
        constructor() {
            this.apiKey = GM_getValue('ups-itemmarket-api-key', '');
            this.data = [];
            this.initialized = false;
            this.setupProfileUI();
        }

        setupProfileUI() {
            const currentProfileId = this.getCurrentProfileId();
            if (!currentProfileId) return;

            const pollInterval = 150;
            const maxWait = 10000;
            let waited = 0;

            const id = setInterval(() => {
                const profileLink = document.querySelector('.settings-menu > .link > a:first-child');
                if (profileLink && profileLink.href) {
                    const m = profileLink.href.match(/XID=(\d+)/);
                    const localId = m ? m[1] : null;
                    if (localId && localId === currentProfileId) {
                        this.createAccordion();
                    } else {
                        console.log('[UpsAfkMarket] Not my profile — skipping accordion.');
                    }
                    clearInterval(id);
                    return;
                }
                waited += pollInterval;
                if (waited >= maxWait) {
                    console.warn('[UpsAfkMarket] Timeout waiting for profile link.');
                    clearInterval(id);
                }
            }, pollInterval);
        }

        getCurrentProfileId() {
            const m = window.location.href.match(/XID=(\d+)/);
            return m ? m[1] : null;
        }

        async refreshData() {
            const resultDiv = document.getElementById('itemmarket-results');
            if (!resultDiv) {
                console.warn('[AFK Market] No result container found for refresh.');
                return;
            }

            resultDiv.innerHTML = '<p>⏳ Refreshing market data...</p>';

            try {
                const items = await this.fetchItemMarket();

                if (!items || !items.length) {
                    resultDiv.innerHTML = '<p>No items found on your market.</p>';
                    return;
                }

                const tableHtml = this.renderTable(items);
                resultDiv.innerHTML = tableHtml;
                this.initialized = true;

                (window.Ups?.showToast?.('✅ Market data refreshed!')) ||
                console.log('[AFK Market] Market data refreshed.');
            } catch (err) {
                console.error('[AFK Market] Refresh error:', err);
                resultDiv.innerHTML = `<p style="color:red;">❌ Error fetching data: ${err.message || err}</p>`;
            }
        }

        createAccordion() {
            if (document.querySelector('#ups-itemmarket-settings')) return;

            const profileWrapper = document.querySelector('.profile-wrapper') || document.querySelector('h4#skip-to-content.left')?.parentElement;
            if (!profileWrapper) return;

            const style = document.createElement('style');
            style.textContent = `
        details.ups-settings-accordion {
          margin: 12px 0 0 0;
          border: 1px solid #2d2d2d;
          border-radius: 8px;
          background: #1e1e1e;
          color: #e9e9e9;
          overflow: hidden;
        }
        details.ups-settings-accordion > summary {
          list-style: none;
          cursor: pointer;
          padding: 10px 14px;
          font-weight: 700;
          user-select: none;
          display: flex;
          align-items: center;
          gap: 8px;
          background: #242424;
          border-bottom: 1px solid #2d2d2d;
        }
        details.ups-settings-accordion > summary::-webkit-details-marker { display: none; }
        details.ups-settings-accordion > summary:before {
          content: "▸";
          transition: transform .15s ease;
          font-size: 12px;
          opacity: .9;
        }
        details.ups-settings-accordion[open] > summary:before { transform: rotate(90deg); }
        .ups-settings-content {
          padding: 12px 14px 14px;
          display: grid;
          grid-template-columns: 1fr;
          gap: 12px;
          font-family: Consolas, Menlo, monospace;
        }
        .ups-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
        .ups-btn { padding: 6px 12px; border: none; border-radius: 6px; cursor: pointer; font-weight: 700; }
        .ups-btn.primary { background: #00bcd4; color: #000; }
        .ups-btn.ghost { background: #424242; color: #fff; }
        .ups-table {
          width: 100%;
          border-collapse: collapse;
          margin-top: 10px;
          border-radius: 6px;
          overflow: hidden;
        }

        .ups-table th, .ups-table td {
            border: 1px solid #3a3a3a;
            padding: 10px 8px;
            text-align: center;
            color: #f0f0f0;
        }

        .ups-table th {
          background: #2c2c2c;
          font-weight: 700;
          letter-spacing: 0.3px;
        }

        .ups-table tbody tr:nth-child(odd) {
          background: #242424;
        }

        .ups-table tbody tr:nth-child(even) {
          background: #1e1e1e;
        }

        .ups-table .ups-type-row {
          background: linear-gradient(90deg, #263238 0%, #1e272c 100%) !important;
        }

        .ups-table .ups-type-row:hover {
          background: linear-gradient(90deg, #2f3e44 0%, #263238 100%) !important;
        }

        .ups-table tbody tr:hover {
          background: #333;
          transition: background 0.2s ease;
        }

        .ups-table tbody tr td {
          padding-top: 10px;
          padding-bottom: 10px;
        }

        .ups-total {
          margin-top: 18px;
          font-weight: 700;
          text-align: right;
          border-top: 1px solid #3a3a3a;
          padding-top: 8px;
        }

        .ups-field input[type="text"] {
          width: 100%;
          padding: 8px;
          border: 1px solid #3a3a3a;
          border-radius: 6px;
          background: #2b2b2b;
          color: #eee;
          outline: none;
        }

        #ups-itemmarket-apiKey {
          filter: blur(4px);
          transition: filter 0.2s ease;
        }

        #ups-itemmarket-apiKey:focus {
          filter: none;
        }

        .ups-type-row {
          font-weight: 700;
          color: #4dd0e1;
          cursor: pointer;
          transition: background 0.2s ease, color 0.2s ease;
        }

        .ups-type-row:hover {
          background: linear-gradient(90deg, #2f3e44 0%, #263238 100%);
          color: #80deea;
        }

        .ups-type-row td {
          border-top: 2px solid #3f4a4d;
          border-bottom: 2px solid #3f4a4d;
        }

        .ups-arrow {
          display: inline-block;
          margin-right: 6px;
          transition: transform 0.2s ease;
          font-size: 12px;
          color: #80deea;
        }

        .ups-item-row {
          background: #1b1b1b;
        }

        .ups-item-row:nth-child(odd) {
          background: #202020;
        }

        .ups-item-row td {
          padding-top: 8px;
          padding-bottom: 8px;
        }
      `;
            document.head.appendChild(style);

            const details = document.createElement('details');
            details.className = 'ups-settings-accordion';
            details.id = 'ups-itemmarket-settings';
            details.innerHTML = `
        <summary>💼 Ups - AFK Market</summary>
        <div class="ups-settings-content">
          <div class="ups-field">
            <label>API Key</label>
            <input id="ups-itemmarket-apiKey" type="text" placeholder="Enter API Key...">
          </div>
          <div class="ups-actions">
            <button id="ups-itemmarket-save" class="ups-btn primary" type="button">Save</button>
          </div>
          <div id="ups-itemmarket-results"></div>
        </div>
      `;
            profileWrapper.parentNode.insertBefore(details, profileWrapper.nextSibling);

            const apiInput = document.getElementById('ups-itemmarket-apiKey');
            const saveBtn = document.getElementById('ups-itemmarket-save');
            const resultDiv = document.getElementById('ups-itemmarket-results');

            apiInput.value = this.apiKey;
            const self = this;

            saveBtn.addEventListener('click', async () => {
                const newKey = apiInput.value.trim();
                GM_setValue('ups-itemmarket-api-key', newKey);
                this.apiKey = newKey;
                await this.loadMarketData(resultDiv);
                this.initialized = true;
            });

            details.addEventListener('toggle', async () => {
                if (details.open && !this.initialized) {
                    await this.loadMarketData(resultDiv);
                    this.initialized = true;
                }
            });
        }

        slugifyType(type) {
            return type.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
        }

        async fetchItemMarket(link = null) {
            if (!this.apiKey) throw new Error('API Key not configured.');

            const url = link || `https://api.torn.com/v2/user/itemmarket?offset=0&key=${this.apiKey}`;

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET', url, onload: async (response) => {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.error) return reject(data.error);

                            const currentItems = data.itemmarket || [];

                            // Vérifie s’il y a une page suivante
                            const nextLink = data._metadata?.links?.next;
                            if (nextLink) {
                                const nextItems = await this.fetchItemMarket(nextLink);
                                resolve([...currentItems, ...nextItems]);
                            } else {
                                resolve(currentItems);
                            }
                        } catch (e) {
                            reject(e);
                        }
                    }, onerror: reject,
                });
            });
        }

        attachTypeRowListeners() {
            const rows = document.querySelectorAll('.ups-type-row');
            rows.forEach(row => {
                row.addEventListener('click', () => {
                    const typeSlug = row.dataset.type;
                    const items = document.querySelectorAll(`.ups-item-${CSS.escape(typeSlug)}`);
                    const expanded = row.classList.toggle('expanded');

                    items.forEach(r => {
                        r.style.display = expanded ? 'table-row' : 'none';
                    });

                    const arrow = row.querySelector('.ups-arrow');
                    if (arrow) arrow.style.transform = expanded ? 'rotate(90deg)' : 'rotate(0deg)';
                });
            });
        }

        async loadMarketData(container) {
            container.innerHTML = '<p>⏳ Loading market data...</p>';
            try {
                const items = await this.fetchItemMarket();
                if (!items || !items.length) {
                    container.innerHTML = '<p>No items found on your market.</p>';
                    return;
                }
                const tableHtml = this.renderTable(items);
                container.innerHTML = tableHtml;
            } catch (e) {
                container.innerHTML = `<p style="color:red;">❌ Error: ${e.message || e}</p>`;
            }
        }

        formatNumber(num) {
            if (isNaN(num)) return num;
            return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        }

        renderTable(rows) {
            const grouped = {};
            for (const r of rows) {
                const type = r.item?.type || 'Unknown';
                if (!grouped[type]) grouped[type] = [];
                grouped[type].push(r);
            }

            const grandTotal = rows.reduce((acc, r) => acc + (r.price * r.amount), 0);

            const tableRows = Object.entries(grouped).map(([type, items]) => {
                const typeTotal = items.reduce((acc, r) => acc + (r.price * r.amount), 0);
                const typeCount = items.length;

                const typeRow = `
  <tr class="ups-type-row" data-type="${this.slugifyType(type)}">
    <td style="text-align: left; padding-left: 32px;"><span class="ups-arrow">▸</span><strong>${type}</strong></td>
    <td>${this.formatNumber(typeCount)}</td>
    <td>—</td>
    <td>$${this.formatNumber(typeTotal)}</td>
  </tr>
`;

                const itemRows = items.map(r => `
      <tr class="ups-item-row ups-item-${this.slugifyType(type)}" style="display:none;">
        <td>${r.item?.name || 'Unknown'}</td>
        <td>${this.formatNumber(r.amount)}</td>
        <td>$${this.formatNumber(r.price)}</td>
        <td>$${this.formatNumber((r.price * r.amount))}</td>
      </tr>
    `).join('');

                return typeRow + itemRows;
            }).join('');

            const table = `
    <table class="ups-table">
      <thead>
        <tr>
          <th>Item / Type</th>
          <th>Quantity</th>
          <th>Price (each)</th>
          <th>Total</th>
        </tr>
      </thead>
      <tbody>${tableRows}</tbody>
    </table>
    <div class="ups-total">💰 Grand Total: ${grandTotal.toLocaleString()} $</div>
  `;

            setTimeout(() => this.attachTypeRowListeners(), 0);
            return table;
        }
    }

    new TornItemMarketAnalyzer();
})();