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!

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();