您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Floating summaries of faction online and status info on Torn.com, with API key input, persistent positions, settings, two main boxes with clickable statuses opening separate popups for names (independent for each faction), popups refresh with API, optional second faction tracking, chain info panel with dynamic font sizing and functional emergency hit buttons, and multi-page hiding.
// ==UserScript== // @name Torn Faction Summaries // @namespace http://tampermonkey.net/ // @version 1.17 // @description Floating summaries of faction online and status info on Torn.com, with API key input, persistent positions, settings, two main boxes with clickable statuses opening separate popups for names (independent for each faction), popups refresh with API, optional second faction tracking, chain info panel with dynamic font sizing and functional emergency hit buttons, and multi-page hiding. // @author Rufus // @match *://www.torn.com/* // @match *://torn.com/* // @icon https://www.torn.com/favicon.ico // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // Config let API_KEY = localStorage.getItem('tornApiKey') || ''; const USER_FACTION_ID = ''; // User's faction ID (leave blank if dynamic) const settings = JSON.parse(localStorage.getItem('factionSummarySettings')) || { lockPosition: true, trackSecondFaction: false, secondFactionId: '', showChainInfo: false, hideOnPages: [], emergencyHit1: '', emergencyHit2: '' }; const UPDATE_INTERVAL = 30000; const DEFAULT_POSITIONS = { userOnline: { top: '10px', left: '10px' }, userStatus: { top: '50px', left: '10px' }, secondOnline: { top: '10px', left: '250px' }, secondStatus: { top: '50px', left: '250px' }, chainInfo: { top: '10px', left: '500px' }, popupOnline: { top: '100px', left: '200px' }, popupIdle: { top: '120px', left: '220px' }, popupOffline: { top: '140px', left: '240px' }, popupOkay: { top: '160px', left: '260px' }, popupTraveling: { top: '180px', left: '280px' }, popupAbroad: { top: '200px', left: '300px' }, popupHospital: { top: '220px', left: '320px' }, popupJail: { top: '240px', left: '340px' }, popupFederal: { top: '260px', left: '360px' } }; const STATUS_COLORS = { 'Online': '#00FF00', 'Idle': '#FFD700', 'Offline': '#FF0000', 'Okay': '#00FF00', 'Traveling': '#00CCFF', 'Abroad': '#00CCFF', 'In Hospital': '#FF0000', 'In Jail': '#FFFFFF', 'Federal': '#808080' }; const TIMEOUT_COLORS = { good: '#00FF00', // 5:00 to 3:30 (300s to 210s) warning: '#FFD700', // 3:30 to 2:00 (210s to 120s) urgent: '#FF0000' // Below 2:00 (<120s) }; // Load saved positions or set defaults const savedPositions = {}; for (const [key, defaultPos] of Object.entries(DEFAULT_POSITIONS)) { savedPositions[key] = settings.lockPosition ? (JSON.parse(localStorage.getItem(`${key}BoxPos`)) || defaultPos) : defaultPos; } // Create Main Boxes const userOnlineBox = document.createElement('div'); userOnlineBox.id = 'user-online-summary-box'; userOnlineBox.style.top = savedPositions.userOnline.top; userOnlineBox.style.left = savedPositions.userOnline.left; document.body.appendChild(userOnlineBox); const userStatusBox = document.createElement('div'); userStatusBox.id = 'user-status-summary-box'; userStatusBox.style.top = savedPositions.userStatus.top; userStatusBox.style.left = savedPositions.userStatus.left; document.body.appendChild(userStatusBox); const secondOnlineBox = document.createElement('div'); secondOnlineBox.id = 'second-online-summary-box'; secondOnlineBox.style.top = savedPositions.secondOnline.top; secondOnlineBox.style.left = savedPositions.secondOnline.left; secondOnlineBox.style.display = settings.trackSecondFaction ? 'block' : 'none'; document.body.appendChild(secondOnlineBox); const secondStatusBox = document.createElement('div'); secondStatusBox.id = 'second-status-summary-box'; secondStatusBox.style.top = savedPositions.secondStatus.top; secondStatusBox.style.left = savedPositions.secondStatus.left; secondStatusBox.style.display = settings.trackSecondFaction ? 'block' : 'none'; document.body.appendChild(secondStatusBox); const chainInfoBox = document.createElement('div'); chainInfoBox.id = 'chain-info-box'; chainInfoBox.style.top = savedPositions.chainInfo.top; chainInfoBox.style.left = savedPositions.chainInfo.left; chainInfoBox.style.display = settings.showChainInfo ? 'block' : 'none'; document.body.appendChild(chainInfoBox); // Create Popup Boxes for User and Second Faction const userPopups = {}; const secondPopups = {}; ['Online', 'Idle', 'Offline', 'Okay', 'Traveling', 'Abroad', 'In Hospital', 'Jail', 'Federal'].forEach(status => { const key = `popup${status.replace('In ', '')}`; // User faction popups userPopups[status] = document.createElement('div'); userPopups[status].id = `user-${status.toLowerCase().replace(' ', '-')}-names-popup`; userPopups[status].style.top = savedPositions[key].top; userPopups[status].style.left = savedPositions[key].left; userPopups[status].style.display = 'none'; userPopups[status].dataset.faction = 'user'; document.body.appendChild(userPopups[status]); // Second faction popups secondPopups[status] = document.createElement('div'); secondPopups[status].id = `second-${status.toLowerCase().replace(' ', '-')}-names-popup`; secondPopups[status].style.top = savedPositions[key].top; secondPopups[status].style.left = `${parseInt(savedPositions[key].left) + 220}px`; // Offset for visibility secondPopups[status].style.display = 'none'; secondPopups[status].dataset.faction = 'second'; document.body.appendChild(secondPopups[status]); }); // Create API Key Input Box const apiInputBox = document.createElement('div'); apiInputBox.id = 'api-input-box'; apiInputBox.innerHTML = ` <div style="background: rgba(26, 26, 26, 0.9); padding: 15px; border: 1px solid #ff00ff; box-shadow: 0 0 10px #ff00ff;"> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;">Enter your Torn API Key:</label><br> <input type="text" id="api-key-input" style="width: 200px; padding: 5px; margin: 10px 0; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;"> <br> <button id="api-save-btn" style="padding: 5px 10px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer;">Save</button> </div> `; apiInputBox.style.cssText = ` position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 10002; display: none; `; document.body.appendChild(apiInputBox); // Create Settings Button const settingsBtn = document.createElement('button'); settingsBtn.id = 'settings-btn'; settingsBtn.textContent = '⚙️'; settingsBtn.style.cssText = ` position: fixed; top: 10px; right: 10px; background: #ff00ff; color: #e0e0e0; border: none; padding: 5px 10px; cursor: pointer; z-index: 10001; font-size: 16px; `; document.body.appendChild(settingsBtn); // Create Settings Panel const settingsPanel = document.createElement('div'); settingsPanel.id = 'settings-panel'; settingsPanel.innerHTML = ` <div style="background: rgba(26, 26, 26, 0.9); padding: 15px; border: 1px solid #ff00ff; box-shadow: 0 0 10px #ff00ff;"> <h3 style="color: #e0e0e0; font-family: 'Courier New', monospace; margin: 0 0 10px;">Settings</h3> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;"> <input type="checkbox" id="lock-position" ${settings.lockPosition ? 'checked' : ''}> Lock Position Across Pages </label><br> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;"> <input type="checkbox" id="track-second-faction" ${settings.trackSecondFaction ? 'checked' : ''}> Track Another Faction </label><br> <div id="second-faction-id-input" style="display: ${settings.trackSecondFaction ? 'block' : 'none'}; margin-top: 10px;"> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;">Second Faction ID:</label><br> <input type="text" id="second-faction-id" value="${settings.secondFactionId || ''}" style="width: 100px; padding: 5px; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;"> </div> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;"> <input type="checkbox" id="show-chain-info" ${settings.showChainInfo ? 'checked' : ''}> Show Chain Info Panel </label><br> <div style="margin-top: 10px;"> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;">Hide on Pages (comma-separated URLs):</label><br> <input type="text" id="hide-on-pages" value="${Array.isArray(settings.hideOnPages) ? settings.hideOnPages.join(', ') : ''}" style="width: 200px; padding: 5px; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;" placeholder="e.g., faction.php, jailview.php"> </div> <div style="margin-top: 10px;"> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;">Emergency Hit 1 URL:</label><br> <input type="text" id="emergency-hit-1" value="${settings.emergencyHit1 || ''}" style="width: 200px; padding: 5px; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;" placeholder="e.g., https://www.torn.com/loader.php?sid=attack&user2ID=12345"> </div> <div style="margin-top: 10px;"> <label style="color: #e0e0e0; font-family: 'Courier New', monospace;">Emergency Hit 2 URL:</label><br> <input type="text" id="emergency-hit-2" value="${settings.emergencyHit2 || ''}" style="width: 200px; padding: 5px; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;" placeholder="e.g., https://www.torn.com/loader.php?sid=attack&user2ID=67890"> </div> <button id="settings-save-btn" style="padding: 5px 10px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer; margin-top: 10px;">Save</button> <button id="settings-close-btn" style="padding: 5px 10px; background: #666; border: none; color: #e0e0e0; cursor: pointer; margin-top: 10px; margin-left: 10px;">Close</button> </div> `; settingsPanel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10002; display: none; `; document.body.appendChild(settingsPanel); // Add CSS for floating boxes and popups const style = document.createElement('style'); style.textContent = ` #user-online-summary-box, #user-status-summary-box, #second-online-summary-box, #second-status-summary-box, #chain-info-box { position: fixed; background: rgba(26, 26, 26, 0.9); color: #e0e0e0; font-family: 'Courier New', monospace; padding: 10px; border: 1px solid #ff00ff; box-shadow: 0 0 10px #ff00ff; z-index: 10000; font-size: 14px; cursor: move; user-select: none; line-height: 1.5; width: auto; } [id^="user-"][id$="-names-popup"], [id^="second-"][id$="-names-popup"] { position: fixed; background: rgba(26, 26, 26, 0.9); color: #e0e0e0; font-family: 'Courier New', monospace; padding: 10px; border: 1px solid #ff00ff; box-shadow: 0 0 10px #ff00ff; z-index: 10003; cursor: move; max-height: 300px; overflow-y: auto; font-size: 16px; width: 200px; } .status-span { cursor: pointer; } .status-span:hover { text-decoration: underline; } .faction-title { margin-bottom: 5px; } .emergency-btn { margin-top: 5px; padding: 5px 10px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer; font-family: 'Courier New', monospace; display: block; } `; document.head.appendChild(style); // Make boxes draggable and save position function makeDraggable(element, storageKey) { let posX = 0, posY = 0, mouseX = 0, mouseY = 0; element.addEventListener('mousedown', dragMouseDown); function dragMouseDown(e) { if (e.target.className !== 'status-span' && !e.target.id.includes('close-') && e.target.className !== 'emergency-btn') { e.preventDefault(); mouseX = e.clientX; mouseY = e.clientY; document.addEventListener('mouseup', closeDragElement); document.addEventListener('mousemove', elementDrag); element.style.zIndex = parseInt(element.style.zIndex || 10000) + 1; } } function elementDrag(e) { e.preventDefault(); posX = mouseX - e.clientX; posY = mouseY - e.clientY; mouseX = e.clientX; mouseY = e.clientY; element.style.top = (element.offsetTop - posY) + "px"; element.style.left = (element.offsetLeft - posX) + "px"; } function closeDragElement() { document.removeEventListener('mouseup', closeDragElement); document.removeEventListener('mousemove', elementDrag); if (settings.lockPosition) { localStorage.setItem(storageKey, JSON.stringify({ top: element.style.top, left: element.style.left })); } } } makeDraggable(userOnlineBox, 'userOnlineBoxPos'); makeDraggable(userStatusBox, 'userStatusBoxPos'); makeDraggable(secondOnlineBox, 'secondOnlineBoxPos'); makeDraggable(secondStatusBox, 'secondStatusBoxPos'); makeDraggable(chainInfoBox, 'chainInfoBoxPos'); Object.entries(userPopups).forEach(([status, popup]) => makeDraggable(popup, `popup${status.replace('In ', '')}BoxPos`)); Object.entries(secondPopups).forEach(([status, popup]) => makeDraggable(popup, `secondPopup${status.replace('In ', '')}BoxPos`)); // Settings Panel Logic settingsBtn.onclick = () => settingsPanel.style.display = 'block'; document.getElementById('settings-close-btn').onclick = () => settingsPanel.style.display = 'none'; document.getElementById('track-second-faction').onchange = (e) => { document.getElementById('second-faction-id-input').style.display = e.target.checked ? 'block' : 'none'; }; document.getElementById('settings-save-btn').onclick = () => { const lockPosition = document.getElementById('lock-position').checked; const trackSecondFaction = document.getElementById('track-second-faction').checked; const secondFactionId = document.getElementById('second-faction-id').value.trim(); const showChainInfo = document.getElementById('show-chain-info').checked; const hideOnPagesInput = document.getElementById('hide-on-pages').value.trim(); const hideOnPages = hideOnPagesInput ? hideOnPagesInput.split(',').map(url => url.trim()).filter(url => url) : []; const emergencyHit1 = document.getElementById('emergency-hit-1').value.trim(); const emergencyHit2 = document.getElementById('emergency-hit-2').value.trim(); settings.lockPosition = lockPosition; settings.trackSecondFaction = trackSecondFaction; settings.secondFactionId = trackSecondFaction ? secondFactionId : ''; settings.showChainInfo = showChainInfo; settings.hideOnPages = hideOnPages; settings.emergencyHit1 = emergencyHit1; settings.emergencyHit2 = emergencyHit2; localStorage.setItem('factionSummarySettings', JSON.stringify(settings)); if (!lockPosition) { ['userOnline', 'userStatus', 'secondOnline', 'secondStatus', 'chainInfo', 'popupOnline', 'popupIdle', 'popupOffline', 'popupOkay', 'popupTraveling', 'popupAbroad', 'popupHospital', 'popupJail', 'popupFederal'].forEach(key => { localStorage.removeItem(`${key}BoxPos`); const element = document.getElementById(key.includes('popup') ? `user-${key.replace('popup', '').toLowerCase()}-names-popup` : `${key}-box`); if (element) { element.style.top = DEFAULT_POSITIONS[key].top; element.style.left = DEFAULT_POSITIONS[key].left; } }); ['Online', 'Idle', 'Offline', 'Okay', 'Traveling', 'Abroad', 'Hospital', 'Jail', 'Federal'].forEach(status => { localStorage.removeItem(`secondPopup${status}BoxPos`); const element = document.getElementById(`second-${status.toLowerCase()}-names-popup`); if (element) { element.style.top = DEFAULT_POSITIONS[`popup${status}`].top; element.style.left = `${parseInt(DEFAULT_POSITIONS[`popup${status}`].left) + 220}px`; } }); } secondOnlineBox.style.display = trackSecondFaction ? 'block' : 'none'; secondStatusBox.style.display = trackSecondFaction ? 'block' : 'none'; chainInfoBox.style.display = showChainInfo ? 'block' : 'none'; settingsPanel.style.display = 'none'; updateVisibility(); fetchFactionData(); }; // Online Summary Logic function getOnlineSummary(members, factionName) { const onlineCounts = { Online: 0, Idle: 0, Offline: 0 }; const onlineNames = { Online: [], Idle: [], Offline: [] }; for (const [id, member] of Object.entries(members || {})) { const status = member.last_action?.status || "Offline"; if (status in onlineCounts) { onlineCounts[status]++; onlineNames[status].push(member.name); } } return `<div class="faction-title">${factionName}</div>` + `Online Status: <span class="status-span" style="color: ${STATUS_COLORS.Online}" data-status="Online" data-names="${onlineNames.Online.join(',')}">Online (${onlineCounts.Online})</span>, ` + `<span class="status-span" style="color: ${STATUS_COLORS.Idle}" data-status="Idle" data-names="${onlineNames.Idle.join(',')}">Idle (${onlineCounts.Idle})</span>, ` + `<span class="status-span" style="color: ${STATUS_COLORS.Offline}" data-status="Offline" data-names="${onlineNames.Offline.join(',')}">Offline (${onlineCounts.Offline})</span>`; } // Status Summary Logic function getStatusSummary(members, factionName) { const statusCounts = { Okay: 0, Traveling: 0, Abroad: 0, "In Hospital": 0, "In Jail": 0, Federal: 0 }; const statusNames = { Okay: [], Traveling: [], Abroad: [], "In Hospital": [], "In Jail": [], Federal: [] }; for (const [id, member] of Object.entries(members || {})) { const state = (member.status?.state || "Okay").toLowerCase(); if (state.includes("hospital")) { statusCounts["In Hospital"]++; statusNames["In Hospital"].push(member.name); } else if (state.includes("jail")) { statusCounts["In Jail"]++; statusNames["In Jail"].push(member.name); } else if (state === "okay") { statusCounts.Okay++; statusNames.Okay.push(member.name); } else if (state === "traveling") { statusCounts.Traveling++; statusNames.Traveling.push(member.name); } else if (state === "abroad") { statusCounts.Abroad++; statusNames.Abroad.push(member.name); } else if (state === "federal") { statusCounts.Federal++; statusNames.Federal.push(member.name); } } return `<div class="faction-title">${factionName}</div>` + `Status:<br>` + `<span class="status-span" style="color: ${STATUS_COLORS.Okay}" data-status="Okay" data-names="${statusNames.Okay.join(',')}">Okay (${statusCounts.Okay})</span><br>` + `<span class="status-span" style="color: ${STATUS_COLORS.Traveling}" data-status="Traveling" data-names="${statusNames.Traveling.join(',')}">Traveling (${statusCounts.Traveling})</span><br>` + `<span class="status-span" style="color: ${STATUS_COLORS.Abroad}" data-status="Abroad" data-names="${statusNames.Abroad.join(',')}">Abroad (${statusCounts.Abroad})</span><br>` + `<span class="status-span" style="color: ${STATUS_COLORS['In Hospital']}" data-status="In Hospital" data-names="${statusNames['In Hospital'].join(',')}">In Hospital (${statusCounts["In Hospital"]})</span><br>` + `<span class="status-span" style="color: ${STATUS_COLORS['In Jail']}" data-status="In Jail" data-names="${statusNames['In Jail'].join(',')}">In Jail (${statusCounts["In Jail"]})</span><br>` + `<span class="status-span" style="color: ${STATUS_COLORS.Federal}" data-status="Federal" data-names="${statusNames.Federal.join(',')}">Federal (${statusCounts.Federal})</span>`; } // Chain Info Logic function getChainInfo(chainData, factionName) { const current = chainData.current || 0; const max = chainData.max || 10; const timeoutSeconds = chainData.timeout || 0; const startTimeUnix = chainData.start || 0; const startTime = startTimeUnix ? new Date(startTimeUnix * 1000).toLocaleString() : 'N/A'; const minutes = Math.floor(timeoutSeconds / 60); const seconds = timeoutSeconds % 60; const timeoutDisplay = `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`; let timeoutColor, timeoutFontSize; if (timeoutSeconds >= 210) { timeoutColor = TIMEOUT_COLORS.good; timeoutFontSize = '14px'; } else if (timeoutSeconds >= 120) { timeoutColor = TIMEOUT_COLORS.warning; timeoutFontSize = '28px'; } else { timeoutColor = TIMEOUT_COLORS.urgent; timeoutFontSize = '56px'; } const now = Math.floor(Date.now() / 1000); const elapsedSeconds = startTimeUnix && current > 0 ? now - startTimeUnix : 0; const elapsedHours = elapsedSeconds / 3600; const hitsPerHour = elapsedHours > 0 ? Math.round(current / elapsedHours) : 0; const milestones = [10, 25, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000]; const nextMilestone = milestones.find(m => m > current) || 'Max'; let timeToNext = 'N/A'; if (hitsPerHour > 0 && nextMilestone !== 'Max' && timeoutSeconds > 0) { const hitsNeeded = nextMilestone - current; const hoursToNext = hitsNeeded / hitsPerHour; const totalMinutes = Math.round(hoursToNext * 60); const days = Math.floor(totalMinutes / (24 * 60)); const hours = Math.floor((totalMinutes % (24 * 60)) / 60); const minutes = totalMinutes % 60; timeToNext = `${days} days, ${hours} hours, ${minutes} min`; } const html = `<div class="faction-title">${factionName} Chain Info</div>` + `<div>Current Chain: <span style="color: ${STATUS_COLORS.Online}">${current}</span></div>` + `<div>Milestone: <span style="color: ${STATUS_COLORS.Okay}">${nextMilestone}</span></div>` + `<div>Timeout: <span style="color: ${timeoutColor}; font-size: ${timeoutFontSize}">${timeoutDisplay}</span></div>` + `<div>Start Time: <span style="color: ${STATUS_COLORS.Traveling}">${startTime}</span></div>` + `<div>Hits/Hour: <span style="color: ${STATUS_COLORS.Abroad}">${hitsPerHour}</span></div>` + `<div>Next Milestone ETA: <span style="color: ${STATUS_COLORS['In Hospital']}">${timeToNext}</span></div>`; chainInfoBox.innerHTML = html; if (settings.emergencyHit1) { const btn1 = document.createElement('button'); btn1.className = 'emergency-btn'; btn1.textContent = 'Emergency Hit 1'; btn1.addEventListener('click', () => window.location.href = settings.emergencyHit1); chainInfoBox.appendChild(btn1); } if (settings.emergencyHit2) { const btn2 = document.createElement('button'); btn2.className = 'emergency-btn'; btn2.textContent = 'Emergency Hit 2'; btn2.addEventListener('click', () => window.location.href = settings.emergencyHit2); chainInfoBox.appendChild(btn2); } return html; } // Show/Update Popup function showPopup(status, names, faction) { const displayStatus = status === "In hospital" ? "In Hospital" : status === "In jail" ? "In Jail" : status; const color = STATUS_COLORS[displayStatus] || '#e0e0e0'; const popup = faction === 'user' ? userPopups[displayStatus] : secondPopups[displayStatus]; const closeBtnId = `${faction}-${status.toLowerCase().replace(' ', '-')}-close-btn`; const nameList = names && names.trim() ? names.split(',').map(name => `<div style="color: ${color}">${name.trim()}</div>`).join('') : 'No members'; popup.innerHTML = `<span style="color: ${color}">${displayStatus}</span>:<br>${nameList}<button id="${closeBtnId}" style="margin-top: 10px; padding: 5px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer;">Close</button>`; popup.style.display = 'block'; document.getElementById(closeBtnId).onclick = () => popup.style.display = 'none'; } // Refresh Open Popups function refreshOpenPopups(faction, data, popupSet) { Object.entries(popupSet).forEach(([status, popup]) => { if (popup.style.display === 'block') { const content = status === "Online" || status === "Idle" || status === "Offline" ? getOnlineSummary(data.members, data.name || (faction === 'user' ? 'Your Faction' : 'Second Faction')) : getStatusSummary(data.members, data.name || (faction === 'user' ? 'Your Faction' : 'Second Faction')); const span = content.match(new RegExp(`data-status="${status.replace('In hospital', 'In Hospital').replace('In jail', 'In Jail')}" data-names="([^"]*)"`, 'i')); if (span) showPopup(status, span[1], faction); } }); } // Update visibility based on current page function updateVisibility() { const currentUrl = window.location.href; const hideOnPages = Array.isArray(settings.hideOnPages) ? settings.hideOnPages : []; const shouldHide = hideOnPages.length > 0 && hideOnPages.some(page => page && currentUrl.includes(page)); console.log('Visibility Check:', { currentUrl, hideOnPages, shouldHide }); userOnlineBox.style.display = shouldHide ? 'none' : 'block'; userStatusBox.style.display = shouldHide ? 'none' : 'block'; secondOnlineBox.style.display = shouldHide ? 'none' : (settings.trackSecondFaction ? 'block' : 'none'); secondStatusBox.style.display = shouldHide ? 'none' : (settings.trackSecondFaction ? 'block' : 'none'); chainInfoBox.style.display = shouldHide ? 'none' : (settings.showChainInfo ? 'block' : 'none'); settingsBtn.style.display = shouldHide ? 'none' : 'block'; if (!API_KEY && !shouldHide) apiInputBox.style.display = 'block'; else apiInputBox.style.display = 'none'; } // Fetch Faction Data let latestUserData = null; let latestSecondData = null; async function fetchFactionData() { updateVisibility(); const hideOnPages = Array.isArray(settings.hideOnPages) ? settings.hideOnPages : []; if (hideOnPages.length > 0 && hideOnPages.some(page => page && window.location.href.includes(page))) { console.log('Skipping fetch due to page hide:', window.location.href); return; } try { let userFactionUrl = `https://api.torn.com/faction/${USER_FACTION_ID}?selections=basic&key=${API_KEY}`; const userResponse = await fetch(userFactionUrl); if (!userResponse.ok) throw new Error(`User Faction HTTP error ${userResponse.status}`); const userFactionData = await userResponse.json(); if (userFactionData.error) throw new Error(`User Faction API Error: ${userFactionData.error.error}`); const userFactionName = userFactionData.name || 'Your Faction'; latestUserData = userFactionData; userOnlineBox.innerHTML = getOnlineSummary(userFactionData.members, userFactionName); userStatusBox.innerHTML = getStatusSummary(userFactionData.members, userFactionName); if (settings.showChainInfo) { let chainUrl = `https://api.torn.com/faction/${USER_FACTION_ID}?selections=chain&key=${API_KEY}`; const chainResponse = await fetch(chainUrl); if (!chainResponse.ok) throw new Error(`Chain HTTP error ${chainResponse.status}`); const chainData = await chainResponse.json(); if (chainData.error) throw new Error(`Chain API Error: ${chainData.error.error}`); getChainInfo(chainData.chain, userFactionName); } if (settings.trackSecondFaction && settings.secondFactionId) { let secondFactionUrl = `https://api.torn.com/faction/${settings.secondFactionId}?selections=basic&key=${API_KEY}`; const secondResponse = await fetch(secondFactionUrl); if (!secondResponse.ok) throw new Error(`Second Faction HTTP error ${secondResponse.status}`); const secondFactionData = await secondResponse.json(); if (secondFactionData.error) throw new Error(`Second Faction API Error: ${secondFactionData.error.error}`); const secondFactionName = secondFactionData.name || 'Second Faction'; latestSecondData = secondFactionData; secondOnlineBox.innerHTML = getOnlineSummary(secondFactionData.members, secondFactionName); secondStatusBox.innerHTML = getStatusSummary(secondFactionData.members, secondFactionName); } document.querySelectorAll('.status-span').forEach(span => { span.onclick = () => { const status = span.getAttribute('data-status'); const names = span.getAttribute('data-names'); const faction = span.closest('[id^="user-"]') ? 'user' : 'second'; showPopup(status, names, faction); }; }); if (latestUserData) refreshOpenPopups('user', latestUserData, userPopups); if (settings.trackSecondFaction && latestSecondData) refreshOpenPopups('second', latestSecondData, secondPopups); } catch (error) { console.error('Faction Fetch Error:', error.message); userOnlineBox.innerHTML = '<div class="faction-title">Error</div>Online Status: <span style="color: red">Error</span>'; userStatusBox.innerHTML = '<div class="faction-title">Error</div>Status: <span style="color: red">Error</span>'; if (settings.trackSecondFaction) { secondOnlineBox.innerHTML = '<div class="faction-title">Error</div>Online Status: <span style="color: red">Error</span>'; secondStatusBox.innerHTML = '<div class="faction-title">Error</div>Status: <span style="color: red">Error</span>'; } if (settings.showChainInfo) chainInfoBox.innerHTML = '<div class="faction-title">Error</div><span style="color: red">Error</span>'; showApiInput(); } } // Show API Key Input function showApiInput() { apiInputBox.style.display = 'block'; userOnlineBox.style.display = 'none'; userStatusBox.style.display = 'none'; secondOnlineBox.style.display = 'none'; secondStatusBox.style.display = 'none'; chainInfoBox.style.display = 'none'; settingsBtn.style.display = 'none'; } // Hide API Key Input function hideApiInput() { apiInputBox.style.display = 'none'; updateVisibility(); } // Save API Key and Test document.getElementById('api-save-btn').addEventListener('click', async () => { const newApiKey = document.getElementById('api-key-input').value.trim(); if (!newApiKey) { alert('Please enter a valid API key.'); return; } API_KEY = newApiKey; localStorage.setItem('tornApiKey', API_KEY); try { let testUrl = `https://api.torn.com/faction/${USER_FACTION_ID}?selections=basic&key=${API_KEY}`; const response = await fetch(testUrl); const data = await response.json(); if (data.error) throw new Error(`API Error: ${data.error.error}`); hideApiInput(); fetchFactionData(); setInterval(fetchFactionData, UPDATE_INTERVAL); } catch (error) { console.error('API Key Test Failed:', error.message); alert('Invalid API key: ' + error.message); } }); // Handle navigation within Torn let lastUrl = window.location.href; new MutationObserver(() => { const currentUrl = window.location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; updateVisibility(); fetchFactionData(); } }).observe(document, { subtree: true, childList: true }); // Initial Load Logic if (!API_KEY) showApiInput(); else { updateVisibility(); fetchFactionData(); setInterval(fetchFactionData, UPDATE_INTERVAL); } })();