// ==UserScript==
// @name Torn Faction Summaries
// @namespace http://tampermonkey.net/
// @version 1.12
// @description Floating summaries of faction online and status info on Torn.com, with API key input, persistent positions, settings, clickable statuses showing member names, optional second faction tracking, chain info panel, and page-specific hiding.
// @author Roofis
// @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,
hideOnPage: '' // New setting for hiding on specific page
};
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' }
};
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 savedUserOnlinePos = settings.lockPosition ? (JSON.parse(localStorage.getItem('userOnlineBoxPos')) || DEFAULT_POSITIONS.userOnline) : DEFAULT_POSITIONS.userOnline;
const savedUserStatusPos = settings.lockPosition ? (JSON.parse(localStorage.getItem('userStatusBoxPos')) || DEFAULT_POSITIONS.userStatus) : DEFAULT_POSITIONS.userStatus;
const savedSecondOnlinePos = settings.lockPosition ? (JSON.parse(localStorage.getItem('secondOnlineBoxPos')) || DEFAULT_POSITIONS.secondOnline) : DEFAULT_POSITIONS.secondOnline;
const savedSecondStatusPos = settings.lockPosition ? (JSON.parse(localStorage.getItem('secondStatusBoxPos')) || DEFAULT_POSITIONS.secondStatus) : DEFAULT_POSITIONS.secondStatus;
const savedChainInfoPos = settings.lockPosition ? (JSON.parse(localStorage.getItem('chainInfoBoxPos')) || DEFAULT_POSITIONS.chainInfo) : DEFAULT_POSITIONS.chainInfo;
// Create User Faction Boxes
const userOnlineBox = document.createElement('div');
userOnlineBox.id = 'user-online-summary-box';
userOnlineBox.style.top = savedUserOnlinePos.top;
userOnlineBox.style.left = savedUserOnlinePos.left;
document.body.appendChild(userOnlineBox);
const userStatusBox = document.createElement('div');
userStatusBox.id = 'user-status-summary-box';
userStatusBox.style.top = savedUserStatusPos.top;
userStatusBox.style.left = savedUserStatusPos.left;
document.body.appendChild(userStatusBox);
// Create Second Faction Boxes (hidden by default)
const secondOnlineBox = document.createElement('div');
secondOnlineBox.id = 'second-online-summary-box';
secondOnlineBox.style.top = savedSecondOnlinePos.top;
secondOnlineBox.style.left = savedSecondOnlinePos.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 = savedSecondStatusPos.top;
secondStatusBox.style.left = savedSecondStatusPos.left;
secondStatusBox.style.display = settings.trackSecondFaction ? 'block' : 'none';
document.body.appendChild(secondStatusBox);
// Create Chain Info Box (hidden by default)
const chainInfoBox = document.createElement('div');
chainInfoBox.id = 'chain-info-box';
chainInfoBox.style.top = savedChainInfoPos.top;
chainInfoBox.style.left = savedChainInfoPos.left;
chainInfoBox.style.display = settings.showChainInfo ? 'block' : 'none';
document.body.appendChild(chainInfoBox);
// Create API Key Input Box (moved to top center)
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 Page (URL):</label><br>
<input type="text" id="hide-on-page" value="${settings.hideOnPage || ''}" style="width: 200px; padding: 5px; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;" placeholder="e.g., www.torn.com/faction.php">
</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);
// Create Names Popup Box
const namesPopup = document.createElement('div');
namesPopup.id = 'names-popup';
namesPopup.style.cssText = `
position: fixed;
top: 100px;
left: 200px;
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;
display: none;
cursor: move;
max-height: 300px;
overflow-y: auto;
font-size: 16px;
`;
document.body.appendChild(namesPopup);
// Add CSS for floating boxes
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;
}
.status-span {
cursor: pointer;
}
.status-span:hover {
text-decoration: underline;
}
.faction-title {
margin-bottom: 5px;
}
`;
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 !== 'close-names-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');
makeDraggable(namesPopup, 'namesPopupPos');
// 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 hideOnPage = document.getElementById('hide-on-page').value.trim();
settings.lockPosition = lockPosition;
settings.trackSecondFaction = trackSecondFaction;
settings.secondFactionId = trackSecondFaction ? secondFactionId : '';
settings.showChainInfo = showChainInfo;
settings.hideOnPage = hideOnPage;
localStorage.setItem('factionSummarySettings', JSON.stringify(settings));
if (!lockPosition) {
localStorage.removeItem('userOnlineBoxPos');
localStorage.removeItem('userStatusBoxPos');
localStorage.removeItem('secondOnlineBoxPos');
localStorage.removeItem('secondStatusBoxPos');
localStorage.removeItem('chainInfoBoxPos');
userOnlineBox.style.top = DEFAULT_POSITIONS.userOnline.top;
userOnlineBox.style.left = DEFAULT_POSITIONS.userOnline.left;
userStatusBox.style.top = DEFAULT_POSITIONS.userStatus.top;
userStatusBox.style.left = DEFAULT_POSITIONS.userStatus.left;
secondOnlineBox.style.top = DEFAULT_POSITIONS.secondOnline.top;
secondOnlineBox.style.left = DEFAULT_POSITIONS.secondOnline.left;
secondStatusBox.style.top = DEFAULT_POSITIONS.secondStatus.top;
secondStatusBox.style.left = DEFAULT_POSITIONS.secondStatus.left;
chainInfoBox.style.top = DEFAULT_POSITIONS.chainInfo.top;
chainInfoBox.style.left = DEFAULT_POSITIONS.chainInfo.left;
}
secondOnlineBox.style.display = trackSecondFaction ? 'block' : 'none';
secondStatusBox.style.display = trackSecondFaction ? 'block' : 'none';
chainInfoBox.style.display = showChainInfo ? 'block' : 'none';
settingsPanel.style.display = 'none';
updateVisibility(); // Update visibility after saving settings
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; // Default to 10 if no chain active
const timeoutSeconds = chainData.timeout || 0;
const startTimeUnix = chainData.start || 0;
const startTime = startTimeUnix ? new Date(startTimeUnix * 1000).toLocaleString() : 'N/A';
const cooldown = chainData.cooldown || 0;
// Convert timeout to minutes:seconds
const minutes = Math.floor(timeoutSeconds / 60);
const seconds = timeoutSeconds % 60;
const timeoutDisplay = `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
// Determine timeout color
let timeoutColor;
if (timeoutSeconds >= 210) { // 3:30 to 5:00
timeoutColor = TIMEOUT_COLORS.good;
} else if (timeoutSeconds >= 120) { // 2:00 to 3:30
timeoutColor = TIMEOUT_COLORS.warning;
} else { // Below 2:00
timeoutColor = TIMEOUT_COLORS.urgent;
}
// Calculate hits per hour: (elapsed hours / total hits) inverted to hits/hour
const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
const elapsedSeconds = startTimeUnix && current > 0 ? now - startTimeUnix : 0;
const elapsedHours = elapsedSeconds / 3600;
const hitsPerHour = elapsedHours > 0 ? Math.round(current / elapsedHours) : 0;
// Milestones (standard Torn chain tiers)
const milestones = [10, 25, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];
const nextMilestone = milestones.find(m => m > current) || 'Max';
// Estimated time to next milestone in days, hours, minutes
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); // Total minutes to next milestone
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`;
}
return `<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}">${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>`;
}
// Show Names Popup
function showNamesPopup(status, names) {
const color = STATUS_COLORS[status] || '#e0e0e0';
if (!names) {
namesPopup.innerHTML = `<span style="color: ${color}">${status}</span>: No members`;
} else {
const nameList = names.split(',').map(name => `<div style="color: ${color}">${name.trim()}</div>`).join('');
namesPopup.innerHTML = `<span style="color: ${color}">${status}</span>:<br>${nameList}<button id="close-names-btn" style="margin-top: 10px; padding: 5px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer;">Close</button>`;
}
namesPopup.style.display = 'block';
namesPopup.style.zIndex = 10003; // Ensure it stays on top
document.getElementById('close-names-btn').onclick = () => {
namesPopup.style.display = 'none';
};
}
// Update visibility based on current page
function updateVisibility() {
const currentUrl = window.location.href;
const hideOnPage = settings.hideOnPage;
const shouldHide = hideOnPage && currentUrl.includes(hideOnPage);
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';
// API input box visibility is handled separately and not affected by page hiding
if (!API_KEY && !shouldHide) {
apiInputBox.style.display = 'block';
}
}
// Fetch Faction Data
async function fetchFactionData() {
updateVisibility(); // Check visibility before fetching
if (settings.hideOnPage && window.location.href.includes(settings.hideOnPage)) {
return; // Skip fetching if hidden on this page
}
try {
// User Faction Basic Data
let userFactionUrl = `https://api.torn.com/faction/${USER_FACTION_ID}?selections=basic&key=${API_KEY}`;
console.log('Fetching user faction data from:', userFactionUrl);
const userResponse = await fetch(userFactionUrl);
if (!userResponse.ok) {
const text = await userResponse.text();
throw new Error(`User Faction HTTP error ${userResponse.status}: ${text}`);
}
const userFactionData = await userResponse.json();
if (userFactionData.error) throw new Error(`User Faction API Error: ${userFactionData.error.error}`);
console.log('User faction data received:', userFactionData);
const userFactionName = userFactionData.name || 'Your Faction';
userOnlineBox.innerHTML = getOnlineSummary(userFactionData.members, userFactionName);
userStatusBox.innerHTML = getStatusSummary(userFactionData.members, userFactionName);
// User Faction Chain Data
if (settings.showChainInfo) {
let chainUrl = `https://api.torn.com/faction/${USER_FACTION_ID}?selections=chain&key=${API_KEY}`;
console.log('Fetching chain data from:', chainUrl);
const chainResponse = await fetch(chainUrl);
if (!chainResponse.ok) {
const text = await chainResponse.text();
throw new Error(`Chain HTTP error ${chainResponse.status}: ${text}`);
}
const chainData = await chainResponse.json();
if (chainData.error) throw new Error(`Chain API Error: ${chainData.error.error}`);
console.log('Chain data received:', chainData);
chainInfoBox.innerHTML = getChainInfo(chainData.chain, userFactionName);
}
// Second Faction (if enabled)
if (settings.trackSecondFaction && settings.secondFactionId) {
let secondFactionUrl = `https://api.torn.com/faction/${settings.secondFactionId}?selections=basic&key=${API_KEY}`;
console.log('Fetching second faction data from:', secondFactionUrl);
const secondResponse = await fetch(secondFactionUrl);
if (!secondResponse.ok) {
const text = await secondResponse.text();
throw new Error(`Second Faction HTTP error ${secondResponse.status}: ${text}`);
}
const secondFactionData = await secondResponse.json();
if (secondFactionData.error) throw new Error(`Second Faction API Error: ${secondFactionData.error.error}`);
console.log('Second faction data received:', secondFactionData);
const secondFactionName = secondFactionData.name || 'Second Faction';
secondOnlineBox.innerHTML = getOnlineSummary(secondFactionData.members, secondFactionName);
secondStatusBox.innerHTML = getStatusSummary(secondFactionData.members, secondFactionName);
}
// Add click listeners to status spans
document.querySelectorAll('.status-span').forEach(span => {
span.onclick = () => {
const status = span.getAttribute('data-status');
const names = span.getAttribute('data-names');
showNamesPopup(status, names);
};
});
} 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(); // Restore visibility based on page settings
}
// 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);
console.log('API Key saved:', 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 (single-page app behavior)
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);
}
})();