Torn OC ItemEye

Shows current faction members without their required OC item on the CRIMES page. Includes links to IM, and includes custom message with easy copy paste for sending!

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Torn OC ItemEye
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Shows current faction members without their required OC item on the CRIMES page. Includes links to IM, and includes custom message with easy copy paste for sending!
// @author       HeyItzWerty [3626448]
// @match        https://www.torn.com/factions.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @connect      api.torn.com
// ==/UserScript==

(function() {
    'use strict';

    // --- Native Torn UI Styling ---
    GM_addStyle(`
        #oc-item-tool {
            background-color: var(--default-bg-panel-color, #f2f2f2);
            color: var(--default-color, #333);
            border-radius: 5px;
            margin-bottom: 10px;
            font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
            border: 1px solid var(--default-panel-border-color, #cccccc);
            overflow: hidden;
        }

        .oc-header {
            background: var(--default-panel-header-background, linear-gradient(to bottom, #eeeeee 0%, #cccccc 100%));
            color: var(--default-panel-header-color, #333);
            padding: 6px 10px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid var(--default-panel-border-color, #ccc);
            font-weight: 700;
            text-shadow: var(--default-text-shadow, none);
        }

        .oc-header-title { display: flex; align-items: center; }
        
        .oc-header-title img { 
            height: 24px; 
            width: auto; 
            max-width: 150px;
            object-fit: contain; 
        }

        .oc-body { padding: 10px; background: var(--default-bg-panel-color, #fff); }

        .oc-input {
            padding: 4px 8px;
            border-radius: 3px;
            border: 1px solid var(--default-panel-border-color, #ccc);
            background: var(--default-bg-panel-active-color, #fff);
            color: var(--default-color, #333);
            font-size: 11px;
            outline: none;
        }

        .oc-btn { 
            cursor: pointer; 
            padding: 4px 10px; 
            border: 1px solid var(--default-panel-border-color, #ccc); 
            border-radius: 3px; 
            font-weight: bold; 
            font-size: 11px; 
            background: var(--default-bg-panel-active-color, #e0e0e0); 
            color: var(--default-color, #333); 
            transition: all 0.2s ease; 
            text-decoration: none;
            display: inline-flex;
            align-items: center;
            gap: 4px;
        }
        .oc-btn:hover { filter: brightness(0.9); }
        .btn-primary { border-color: #1976D2; color: #fff; background: #2196F3; }
        .btn-secondary { border-color: #388E3C; color: #fff; background: #4CAF50; }

        .icon-btn { cursor: pointer; background: transparent; border: none; color: var(--default-color, #666); padding: 2px; display: flex; align-items: center; opacity: 0.7; transition: opacity 0.2s; }
        .icon-btn:hover { opacity: 1; color: var(--default-blue-color, #005eb8); }

        .oc-table { width: 100%; border-collapse: collapse; font-size: 12px; }
        
        .oc-table th { 
            background: var(--default-table-header-bg, #f2f2f2); 
            padding: 8px 10px; 
            text-align: left; 
            font-weight: bold; 
            border-bottom: 1px solid var(--default-panel-border-color, #ccc); 
            color: #85b200;
            text-shadow: 1px 1px 0px rgba(0,0,0,0.05);
        }
        .dark-mode .oc-table th { color: #85b200; }
        
        .oc-table td { 
            padding: 8px 10px; 
            border-bottom: 1px solid var(--default-panel-border-color, #eee); 
            vertical-align: middle; 
        }
        
        .oc-table tr:nth-child(even) td { background: var(--default-bg-panel-active-color, #f9f9f9); }
        .dark-mode .oc-table tr:nth-child(even) td { background: var(--default-bg-panel-active-color, #2a2a2a); }
        
        .highlight-text { font-weight: 600; color: var(--default-color, #333); }
        .oc-link { color: var(--default-blue-color, #005eb8); text-decoration: none; font-weight: 600; }
        .oc-link:hover { text-decoration: underline; }
    `);

    // SVG Icons
    const ICONS = {
        copy: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`,
        msg: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
        refresh: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`
    };

    const LOGO_URL = "https://i.ibb.co/Zpy216QB/dawdawd.png"; 

    // --- API Handlers ---
    async function getTornData(endpoint, version = "v1") {
        const key = GM_getValue("oc_api_key", "");
        if (!key) throw new Error("API Key required.");
        let baseUrl = version === "v2" ? "https://api.torn.com/v2" : "https://api.torn.com/v1";
        
        // ADDED CACHE BUSTER (_t) to prevent mobile browsers from recycling old data
        let url = `${baseUrl}/${endpoint}${endpoint.includes('?') ? '&' : '?'}key=${key}&_t=${Date.now()}`;
        
        let res = await fetch(url);
        let data = await res.json();
        if (data.error) throw new Error(data.error.error || "API Error");
        return data;
    }

    async function runCheck() {
        const tbody = document.querySelector("#oc-tool-tbody");
        const status = document.querySelector("#oc-status");
        
        const apiKey = GM_getValue("oc_api_key", "");
        
        if (!apiKey) {
            status.innerHTML = "<em>Awaiting API Key...</em>";
            tbody.innerHTML = `
                <tr>
                    <td colspan='4' style='text-align:center; padding:30px;'>
                        <div style="margin-bottom:10px; font-weight:bold; color:var(--default-color, #333);">
                            Please enter a <span style="color:#1976D2;">Limited Access</span> API Key to use Torn OC ItemEye.
                        </div>
                        <input type="password" id="oc-inline-api-input" class="oc-input" placeholder="Paste API Key here..." style="width:250px; margin-right:8px;">
                        <button id="oc-inline-api-save" class="oc-btn btn-primary">Save Key</button>
                    </td>
                </tr>
            `;
            
            document.getElementById('oc-inline-api-save').addEventListener('click', () => {
                const newKey = document.getElementById('oc-inline-api-input').value.trim();
                if(newKey) {
                    GM_setValue("oc_api_key", newKey);
                    runCheck();
                }
            });
            return;
        }

        status.innerHTML = "<em>Scanning planned OCs...</em>";
        tbody.innerHTML = "<tr><td colspan='4' style='text-align:center; padding:20px;'>Pulling live faction data...</td></tr>";

        try {
            const itemsData = await getTornData("torn/?selections=items");
            const items = itemsData.items;
            const factionData = await getTornData("faction/?selections=basic");
            const members = factionData.members || {};
            const crimesData = await getTornData("faction/crimes?cat=planning", "v2");
            
            // Check to ensure they didn't input a Public key (which strips out crime slots)
            if (crimesData.crimes === undefined) {
                throw new Error("Cannot view Faction Crimes. Are you sure you are using a Limited Access key?");
            }

            const crimes = crimesData.crimes || [];

            let missingList = [];
            for (let crime of crimes) {
                if (!crime.slots) continue; // Safety check
                for (let slot of crime.slots) {
                    if (slot.user && slot.item_requirement && slot.item_requirement.is_available === false) {
                        missingList.push({
                            crimeName: crime.name,
                            userId: slot.user.id,
                            userName: members[slot.user.id] ? members[slot.user.id].name : `User`,
                            itemId: slot.item_requirement.id,
                            itemName: items[slot.item_requirement.id]?.name || "Unknown Item"
                        });
                    }
                }
            }

            tbody.innerHTML = "";
            if (missingList.length === 0) {
                tbody.innerHTML = "<tr><td colspan='4' style='text-align:center; padding: 20px; font-weight:bold; color: var(--default-green-color, #4CAF50);'>All assigned members are fully equipped!</td></tr>";
                status.innerText = "Scan complete. No missing items found.";
                return;
            }

            let msgTemplate = GM_getValue("oc_custom_msg", "Hey {user}, noticed you're missing a {item} for the upcoming {crime} OC. Let me know if you need me to send one over!");

            for (let item of missingList) {
                let tr = document.createElement("tr");
                
                let prefillMsg = msgTemplate
                    .replace(/{user}/g, item.userName)
                    .replace(/{item}/g, item.itemName)
                    .replace(/{crime}/g, item.crimeName);

                tr.innerHTML = `
                    <td class="highlight-text">${item.crimeName}</td>
                    <td>
                        <div style="display:flex; align-items:center; gap:8px;">
                            <a class="oc-link" href="https://www.torn.com/profiles.php?XID=${item.userId}" target="_blank">${item.userName} [${item.userId}]</a>
                            <button class="icon-btn copy-btn" title="Copy Name" data-copy="${item.userName} [${item.userId}]">${ICONS.copy}</button>
                            <button class="icon-btn copy-msg-btn" title="Copy message" data-msg="${prefillMsg}">${ICONS.msg}</button>
                        </div>
                    </td>
                    <td class="highlight-text">${item.itemName}</td>
                    <td>
                        <div style="display:flex; gap:6px;">
                            <button class="oc-btn btn-primary btn-buy" data-itemid="${item.itemId}">Market</button>
                            <button class="oc-btn btn-secondary btn-items" title="Go to Items Page">Send Item</button>
                        </div>
                    </td>
                `;
                tbody.appendChild(tr);
            }
            status.innerHTML = `<em>Scan complete. Found ${missingList.length} missing item(s).</em>`;
            attachListeners();
        } catch (e) {
            status.innerHTML = `<span style="color:red; font-weight:bold;">Error: ${e.message}</span>`;
            tbody.innerHTML = `<tr><td colspan='4' style='text-align:center; padding:20px; color:red;'>${e.message}</td></tr>`;
        }
    }

    function attachListeners() {
        document.querySelectorAll('.copy-btn').forEach(btn => {
            btn.addEventListener('click', function() {
                GM_setClipboard(this.getAttribute('data-copy'));
                this.innerHTML = `<span style="color:green; font-weight:bold;">✓</span>`;
                setTimeout(() => this.innerHTML = ICONS.copy, 1000);
            });
        });

        document.querySelectorAll('.copy-msg-btn').forEach(btn => {
            btn.addEventListener('click', function() {
                GM_setClipboard(this.getAttribute('data-msg'));
                this.innerHTML = `<span style="color:green; font-weight:bold;">✓</span>`;
                setTimeout(() => this.innerHTML = ICONS.msg, 1000);
            });
        });

        document.querySelectorAll('.btn-buy').forEach(btn => {
            btn.addEventListener('click', function() {
                window.open(`https://www.torn.com/page.php?sid=ItemMarket#/market/view=search&itemID=${this.dataset.itemid}`, '_blank');
            });
        });

        document.querySelectorAll('.btn-items').forEach(btn => {
            btn.addEventListener('click', () => window.open('https://www.torn.com/item.php', '_blank'));
        });
    }

    function injectUI() {
        if (document.getElementById('oc-item-tool')) return;
        const target = document.querySelector('.factions-crimes-wrap') || document.querySelector('#factions');
        if (!target) return;

        const container = document.createElement('div');
        container.id = 'oc-item-tool';
        container.innerHTML = `
            <div class="oc-header">
                <div class="oc-header-title">
                    <img src="${LOGO_URL}" alt="ItemEye Logo" onerror="this.outerHTML=''">
                    <span style="color: #85b200; font-weight: bold; margin-left: 8px; font-size: 13px; letter-spacing: 0.5px;">Torn OC ItemEye</span>
                </div>
                <button id="oc-refresh-btn" class="oc-btn">${ICONS.refresh} Refresh</button>
            </div>
            <div class="oc-body">
                <div style="display:flex; justify-content: space-between; align-items:flex-end; margin-bottom: 8px;">
                    <span id="oc-status" style="font-size:11px; color: var(--default-color, #666);">Initializing...</span>
                    
                    <div style="display:flex; gap: 12px;">
                        <button id="oc-edit-msg" style="background:none; border:none; color:var(--default-blue-color, #005eb8); font-size:10px; cursor:pointer; text-decoration:underline;">Edit Copy Message</button>
                        <button id="oc-api-reset" style="background:none; border:none; color:var(--default-blue-color, #005eb8); font-size:10px; cursor:pointer; text-decoration:underline;">Reset API Key</button>
                    </div>
                </div>
                <table class="oc-table">
                    <thead><tr><th>Crime</th><th>Member</th><th>Item Needed</th><th>Actions</th></tr></thead>
                    <tbody id="oc-tool-tbody"></tbody>
                </table>
            </div>
        `;
        target.parentNode.insertBefore(container, target);
        
        document.getElementById('oc-refresh-btn').addEventListener('click', runCheck);
        
        document.getElementById('oc-api-reset').addEventListener('click', () => {
            GM_setValue("oc_api_key", "");
            runCheck(); // Will instantly show the inline input box again
        });

        document.getElementById('oc-edit-msg').addEventListener('click', () => {
            let currentMsg = GM_getValue("oc_custom_msg", "Hey {user}, noticed you're missing a {item} for the upcoming {crime} OC. Let me know if you need me to send one over!");
            
            let promptText = "Customize your copy message button for easy pasting when sending an item!\\n" +
                             "{user}  -> Member's name\\n" +
                             "{item}  -> Missing item name\\n" +
                             "{crime}  -> Current OC";

            let newMsg = prompt(promptText, currentMsg);
            
            if (newMsg !== null && newMsg.trim() !== "") {
                GM_setValue("oc_custom_msg", newMsg.trim());
                runCheck(); 
            }
        });
        
        runCheck();
    }

    setInterval(() => {
        if (location.href.includes('tab=crimes') && !document.getElementById('oc-item-tool')) injectUI();
    }, 1500);

})();