Advanced radial navigation with modular search, API integration, mini-apps, and enhanced calibration
// ==UserScript==
// @name Torn Quick-Travel (Radial Menu)
// @namespace http://tampermonkey.net/
// @version 1.6.1
// @description Advanced radial navigation with modular search, API integration, mini-apps, and enhanced calibration
// @author Sensimillia (2168012)
// @match https://www.torn.com/*
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @connect api.torn.com
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ==================== DEBUG MODE ====================
const DEBUG = false;
const log = DEBUG ? console.log.bind(console, '[TornRadial]') : () => {};
// ==================== NOTIFICATION THROTTLER ====================
class NotificationThrottler {
constructor() {
this.lastNotifications = new Map();
this.throttleTime = 2000; // 2 seconds minimum between same notifications
this.maxPerMinute = 10;
this.notificationCount = 0;
this.resetInterval = null;
this.resetInterval = setInterval(() => {
this.notificationCount = 0;
}, 60000);
}
canShow(message) {
if (this.notificationCount >= this.maxPerMinute) {
log('Notification throttled: rate limit reached');
return false;
}
const now = Date.now();
const lastTime = this.lastNotifications.get(message);
if (lastTime && (now - lastTime) < this.throttleTime) {
log('Notification throttled: too soon', message);
return false;
}
this.lastNotifications.set(message, now);
this.notificationCount++;
return true;
}
destroy() {
if (this.resetInterval) {
clearInterval(this.resetInterval);
}
}
}
const NotificationManager = new NotificationThrottler();
// ==================== ERROR LOGGER CLASS ====================
class ErrorLoggerClass {
constructor() {
this.logs = [];
this.maxLogs = 100;
this.loadLogs();
}
log(type, message, error = null) {
const entry = {
timestamp: new Date().toISOString(),
type: type,
message: message,
error: error ? {
name: error.name,
message: error.message,
stack: error.stack
} : null,
userAgent: navigator.userAgent,
screenSize: `${window.innerWidth}x${window.innerHeight}`
};
this.logs.push(entry);
if (this.logs.length > this.maxLogs) this.logs.shift();
try {
localStorage.setItem('tornRadialErrorLogs', JSON.stringify(this.logs));
} catch(e) {
console.error('Failed to save error logs:', e);
}
console.error(`[Torn Radial] ${type.toUpperCase()}: ${message}`, error);
}
loadLogs() {
try {
const stored = localStorage.getItem('tornRadialErrorLogs');
if (stored) this.logs = JSON.parse(stored);
} catch(e) {
console.error('Failed to load error logs:', e);
}
}
getLogs() {
return this.logs;
}
clear() {
this.logs = [];
try {
localStorage.removeItem('tornRadialErrorLogs');
} catch(e) {
console.error('Failed to clear error logs:', e);
}
}
exportLogs() {
const logsText = JSON.stringify(this.logs, null, 2);
const blob = new Blob([logsText], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `torn-radial-errors-${new Date().toISOString()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
const ErrorLogger = new ErrorLoggerClass();
// ==================== STORAGE MANAGER CLASS ====================
class StorageManager {
get(key, defaultValue) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
ErrorLogger.log('error', `Error reading ${key} from localStorage`, e);
return defaultValue;
}
}
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
ErrorLogger.log('error', `Error saving ${key} to localStorage`, e);
return false;
}
}
remove(key) {
try {
localStorage.removeItem(key);
return true;
} catch (e) {
ErrorLogger.log('error', `Error removing ${key} from localStorage`, e);
return false;
}
}
exportAll() {
const data = {
settings: this.get('tornRadialSettings', {}),
loadouts: this.get('tornRadialLoadouts', {}),
position: this.get('tornRadialPosition', {}),
positionMobile: this.get('tornRadialPositionMobile', {}),
usageStats: this.get('tornRadialUsageStats', {}),
timers: this.get('tornRadialTimers', []),
notes: this.get('tornRadialNotes', ''),
searchHistory: this.get('tornRadialSearchHistory', []),
calibration: this.get('tornRadialCalibration', {}),
calibrationMobile: this.get('tornRadialCalibrationMobile', {})
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `torn-radial-backup-${new Date().toISOString()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
importAll(jsonData) {
try {
const data = JSON.parse(jsonData);
if (data.settings) this.set('tornRadialSettings', data.settings);
if (data.loadouts) this.set('tornRadialLoadouts', data.loadouts);
if (data.position) this.set('tornRadialPosition', data.position);
if (data.positionMobile) this.set('tornRadialPositionMobile', data.positionMobile);
if (data.usageStats) this.set('tornRadialUsageStats', data.usageStats);
if (data.timers) this.set('tornRadialTimers', data.timers);
if (data.notes) this.set('tornRadialNotes', data.notes);
if (data.searchHistory) this.set('tornRadialSearchHistory', data.searchHistory);
if (data.calibration) this.set('tornRadialCalibration', data.calibration);
if (data.calibrationMobile) this.set('tornRadialCalibrationMobile', data.calibrationMobile);
return true;
} catch (e) {
ErrorLogger.log('error', 'Failed to import data', e);
return false;
}
}
}
const Storage = new StorageManager();
// ==================== DEVICE DETECTION ====================
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth < 768;
const isPDA = window.innerWidth < 768;
// ==================== TORN API HANDLER CLASS ====================
class TornAPI {
constructor() {
this.baseUrl = 'https://api.torn.com';
this.apiKey = Storage.get('tornRadialApiKey', '');
}
setApiKey(key) {
this.apiKey = key;
Storage.set('tornRadialApiKey', key);
}
async request(endpoint) {
if (!this.apiKey) {
throw new Error('API key not set');
}
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest === 'undefined') {
reject(new Error('GM_xmlhttpRequest not available'));
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: `${this.baseUrl}${endpoint}&key=${this.apiKey}`,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.error) {
reject(new Error(data.error.error));
} else {
resolve(data);
}
} catch (e) {
reject(e);
}
},
onerror: (error) => reject(error)
});
});
}
async getUserInfo(userId) {
return this.request(`/user/${userId}?selections=profile,timestamp`);
}
async getBars() {
return this.request(`/user/?selections=bars`);
}
async getCooldowns() {
return this.request(`/user/?selections=cooldowns`);
}
}
const API = new TornAPI();
// ==================== SEARCH HANDLER CLASS ====================
class SearchHandler {
constructor() {
this.history = Storage.get('tornRadialSearchHistory', []);
this.maxHistory = 20;
}
addToHistory(query, url, type) {
const entry = {
query: query,
url: url,
type: type,
timestamp: Date.now()
};
this.history = this.history.filter(h => h.query !== query);
this.history.unshift(entry);
this.history = this.history.slice(0, this.maxHistory);
Storage.set('tornRadialSearchHistory', this.history);
}
getHistory() {
return this.history;
}
clearHistory() {
this.history = [];
Storage.set('tornRadialSearchHistory', []);
}
async search(query) {
const results = {
players: [],
items: [],
pages: [],
factions: [],
companies: []
};
const queryLower = query.toLowerCase().trim();
// Player ID or name search
if (/^\d+$/.test(query)) {
// Direct player ID
results.players.push({
name: `Player ID: ${query}`,
url: `/profiles.php?XID=${query}`,
icon: '👤',
type: 'player'
});
} else if (query.length >= 3) {
// Player name search
results.players.push({
name: `Search Player: "${query}"`,
url: `/profiles.php?XID=${query}`,
icon: '👤',
type: 'player'
});
}
// Faction search
if (/^\d+$/.test(query)) {
results.factions.push({
name: `Faction ID: ${query}`,
url: `/factions.php?step=profile&ID=${query}`,
icon: '⚔️',
type: 'faction'
});
}
// Company search
if (/^\d+$/.test(query)) {
results.companies.push({
name: `Company ID: ${query}`,
url: `/companies.php?step=profile&ID=${query}`,
icon: '🏢',
type: 'company'
});
}
// Item search - match any query with 2+ characters
if (query.length >= 2) {
results.items.push({
name: `Search Items: "${query}"`,
url: `/imarket.php#/p=shop&step=shop&type=&searchname=${encodeURIComponent(query)}`,
icon: '🛒',
type: 'item'
});
}
// Page matches with better keyword detection
const pageMatches = [
{ keywords: ['gym', 'train', 'stat'], name: 'Gym', url: '/gym.php', icon: '💪' },
{ keywords: ['travel', 'fly', 'airport', 'abroad'], name: 'Travel Agency', url: '/travelagency.php', icon: '✈️' },
{ keywords: ['item', 'inventory', 'inv'], name: 'Items', url: '/item.php', icon: '🎒' },
{ keywords: ['bazaar', 'baz', 'shop'], name: 'Bazaar', url: '/bazaar.php', icon: '🏪' },
{ keywords: ['display', 'cabinet', 'trophy', 'case'], name: 'Display Cabinet', url: '/displaycase.php', icon: '🏆' },
{ keywords: ['faction', 'fac'], name: 'Faction', url: '/factions.php', icon: '⚔️' },
{ keywords: ['crime', 'oc', 'organized'], name: 'Crimes', url: '/crimes.php', icon: '🔫' },
{ keywords: ['hospital', 'hosp', 'revive'], name: 'Hospital', url: '/hospital.php', icon: '🏥' },
{ keywords: ['mission', 'duke'], name: 'Missions', url: '/loader.php?sid=missions', icon: '🎯' },
{ keywords: ['auction'], name: 'Auctions', url: '/auctions.php', icon: '🔨' },
{ keywords: ['message', 'mail', 'inbox'], name: 'Messages', url: '/messages.php', icon: '💬' },
{ keywords: ['event', 'log'], name: 'Events', url: '/events.php', icon: '📅' },
{ keywords: ['forum', 'forums'], name: 'Forums', url: '/forums.php', icon: '💭' },
{ keywords: ['city', 'map'], name: 'City', url: '/city.php', icon: '🏙️' },
{ keywords: ['company', 'job', 'work'], name: 'Job Listings', url: '/joblist.php', icon: '🏢' },
{ keywords: ['property', 'properties', 'home'], name: 'Property', url: '/properties.php', icon: '🏘️' },
{ keywords: ['attack', 'fight', 'log'], name: 'Attack Log', url: '/attacklog.php', icon: '⚡' },
{ keywords: ['bounty', 'bounties'], name: 'Bounties', url: '/bounties.php', icon: '💀' },
{ keywords: ['war', 'rankedwar', 'rw'], name: 'War', url: '/war.php', icon: '💣' },
{ keywords: ['jail', 'prison', 'bust'], name: 'Jail', url: '/jailview.php', icon: '🔒' },
{ keywords: ['newspaper', 'news'], name: 'Newspaper', url: '/newspaper.php', icon: '📰' },
{ keywords: ['home', 'index'], name: 'Home', url: '/index.php', icon: '🏠' },
{ keywords: ['points', 'refill', 'market'], name: 'Points Market', url: '/pmarket.php', icon: '⭐' },
{ keywords: ['race', 'racing', 'track'], name: 'Racing', url: '/loader.php?sid=racing', icon: '🏎️' },
{ keywords: ['casino', 'gamble'], name: 'Casino', url: '/loader.php?sid=casino', icon: '🎰' },
{ keywords: ['education', 'school', 'course'], name: 'Education', url: '/education.php', icon: '📚' },
{ keywords: ['trade'], name: 'Trade', url: '/trade.php', icon: '🤝' },
{ keywords: ['bank', 'vault', 'money'], name: 'Bank', url: '/bank.php', icon: '🏦' },
{ keywords: ['stock', 'stocks', 'exchange'], name: 'Stock Exchange', url: '/stockexchange.php', icon: '📈' },
{ keywords: ['church', 'marry'], name: 'Church', url: '/church.php', icon: '⛪' },
{ keywords: ['museum'], name: 'Museum', url: '/museum.php', icon: '🏛️' },
{ keywords: ['hall', 'fame'], name: 'Hall of Fame', url: '/halloffame.php', icon: '🏆' },
{ keywords: ['competition'], name: 'Competitions', url: '/competition.php', icon: '🎯' }
];
// Find all matching pages
pageMatches.forEach(match => {
if (match.keywords.some(k => queryLower.includes(k))) {
results.pages.push({
name: match.name,
url: match.url,
icon: match.icon,
type: 'page'
});
}
});
return results;
}
}
const SearchManager = new SearchHandler();
// ==================== DYNAMIC THEME SYSTEM ====================
const THEMES = {
torn: {
name: 'Torn',
modalBg: 'rgba(27, 27, 27, 0.98)',
modalHeaderBg: 'rgba(36, 36, 36, 0.95)',
modalFooterBg: 'rgba(34, 34, 34, 0.95)',
sectionBg: 'rgba(36, 36, 36, 0.8)',
inputBg: 'rgba(51, 51, 51, 0.9)',
textColor: '#d0d0d0',
textSecondary: '#9b9b9b',
borderColor: 'rgba(51, 51, 51, 0.5)',
mainBtnBg: 'rgba(36, 36, 36, 0.95)',
mainBtnBorder: 'rgba(74, 163, 223, 0.3)',
primaryColor: '#4aa3df',
accentGradient: 'linear-gradient(135deg, #4aa3df 0%, #66baff 100%)',
dangerColor: '#a33a3a',
successColor: '#3ea34a'
},
light: {
name: 'Light',
modalBg: 'rgba(255, 255, 255, 0.95)',
modalHeaderBg: 'rgba(255, 255, 255, 0.5)',
modalFooterBg: 'rgba(249, 249, 249, 0.8)',
sectionBg: 'rgba(255, 255, 255, 0.7)',
inputBg: 'rgba(120, 120, 128, 0.12)',
textColor: '#000',
textSecondary: '#666',
borderColor: 'rgba(0, 0, 0, 0.08)',
mainBtnBg: 'rgba(255, 255, 255, 0.95)',
mainBtnBorder: 'rgba(0, 0, 0, 0.04)',
primaryColor: '#007AFF',
accentGradient: 'linear-gradient(135deg, #FF2D55 0%, #FF375F 100%)',
dangerColor: '#FF3B30',
successColor: '#34C759'
},
dark: {
name: 'Dark',
modalBg: 'rgba(28, 28, 30, 0.95)',
modalHeaderBg: 'rgba(44, 44, 46, 0.5)',
modalFooterBg: 'rgba(20, 20, 22, 0.8)',
sectionBg: 'rgba(44, 44, 46, 0.7)',
inputBg: 'rgba(255, 255, 255, 0.1)',
textColor: '#FFFFFF',
textSecondary: '#9b9b9b',
borderColor: 'rgba(255, 255, 255, 0.12)',
mainBtnBg: 'rgba(44, 44, 46, 0.95)',
mainBtnBorder: 'rgba(255, 255, 255, 0.08)',
primaryColor: '#0A84FF',
accentGradient: 'linear-gradient(135deg, #FF453A 0%, #FF375F 100%)',
dangerColor: '#FF453A',
successColor: '#32D74B'
},
cyberpunk: {
name: 'Cyberpunk',
modalBg: 'rgba(10, 10, 15, 0.98)',
modalHeaderBg: 'rgba(20, 20, 30, 0.95)',
modalFooterBg: 'rgba(15, 15, 20, 0.95)',
sectionBg: 'rgba(25, 25, 35, 0.8)',
inputBg: 'rgba(35, 35, 50, 0.9)',
textColor: '#00ff9f',
textSecondary: '#7b68ee',
borderColor: 'rgba(0, 255, 159, 0.3)',
mainBtnBg: 'rgba(20, 20, 30, 0.95)',
mainBtnBorder: 'rgba(255, 0, 255, 0.5)',
primaryColor: '#ff00ff',
accentGradient: 'linear-gradient(135deg, #ff00ff 0%, #00ffff 100%)',
dangerColor: '#ff0080',
successColor: '#00ff9f'
},
ocean: {
name: 'Ocean',
modalBg: 'rgba(15, 25, 40, 0.98)',
modalHeaderBg: 'rgba(20, 35, 55, 0.95)',
modalFooterBg: 'rgba(10, 20, 35, 0.95)',
sectionBg: 'rgba(25, 40, 60, 0.8)',
inputBg: 'rgba(30, 50, 75, 0.9)',
textColor: '#e0f4ff',
textSecondary: '#6fa8dc',
borderColor: 'rgba(79, 195, 247, 0.3)',
mainBtnBg: 'rgba(20, 35, 55, 0.95)',
mainBtnBorder: 'rgba(79, 195, 247, 0.5)',
primaryColor: '#4fc3f7',
accentGradient: 'linear-gradient(135deg, #0288d1 0%, #26c6da 100%)',
dangerColor: '#ff6e40',
successColor: '#69f0ae'
},
sunset: {
name: 'Sunset',
modalBg: 'rgba(40, 20, 30, 0.98)',
modalHeaderBg: 'rgba(60, 30, 45, 0.95)',
modalFooterBg: 'rgba(35, 15, 25, 0.95)',
sectionBg: 'rgba(55, 25, 40, 0.8)',
inputBg: 'rgba(70, 35, 50, 0.9)',
textColor: '#ffe0b2',
textSecondary: '#ffab91',
borderColor: 'rgba(255, 138, 101, 0.3)',
mainBtnBg: 'rgba(60, 30, 45, 0.95)',
mainBtnBorder: 'rgba(255, 138, 101, 0.5)',
primaryColor: '#ff8a65',
accentGradient: 'linear-gradient(135deg, #ff6f00 0%, #ff9100 100%)',
dangerColor: '#d50000',
successColor: '#76ff03'
},
neonNoir: {
name: 'Neon Noir',
modalBg: 'rgba(10, 10, 15, 0.98)',
modalHeaderBg: 'rgba(20, 20, 25, 0.95)',
modalFooterBg: 'rgba(15, 15, 20, 0.9)',
sectionBg: 'rgba(25, 25, 30, 0.8)',
inputBg: 'rgba(40, 40, 50, 0.9)',
textColor: '#c0c0ff',
textSecondary: '#8080ff',
borderColor: 'rgba(120, 120, 255, 0.3)',
mainBtnBg: 'rgba(20, 20, 25, 0.95)',
mainBtnBorder: 'rgba(120, 120, 255, 0.4)',
primaryColor: '#9b59b6',
accentGradient: 'linear-gradient(135deg, #8e44ad 0%, #3498db 100%)',
dangerColor: '#e74c3c',
successColor: '#2ecc71'
},
bloodline: {
name: 'Bloodline',
modalBg: 'rgba(15, 10, 10, 0.98)',
modalHeaderBg: 'rgba(25, 15, 15, 0.95)',
modalFooterBg: 'rgba(20, 10, 10, 0.9)',
sectionBg: 'rgba(35, 20, 20, 0.8)',
inputBg: 'rgba(45, 25, 25, 0.9)',
textColor: '#f0b0b0',
textSecondary: '#c07070',
borderColor: 'rgba(255, 80, 80, 0.3)',
mainBtnBg: 'rgba(30, 15, 15, 0.95)',
mainBtnBorder: 'rgba(255, 0, 0, 0.4)',
primaryColor: '#e63946',
accentGradient: 'linear-gradient(135deg, #b71c1c 0%, #f44336 100%)',
dangerColor: '#ff5252',
successColor: '#81c784'
},
stealth: {
name: 'Stealth',
modalBg: 'rgba(8, 8, 8, 0.98)',
modalHeaderBg: 'rgba(12, 12, 12, 0.95)',
modalFooterBg: 'rgba(10, 10, 10, 0.9)',
sectionBg: 'rgba(15, 15, 15, 0.8)',
inputBg: 'rgba(25, 25, 25, 0.9)',
textColor: '#c0c0c0',
textSecondary: '#888',
borderColor: 'rgba(255, 255, 255, 0.05)',
mainBtnBg: 'rgba(18, 18, 18, 0.95)',
mainBtnBorder: 'rgba(255, 255, 255, 0.1)',
primaryColor: '#4a90e2',
accentGradient: 'linear-gradient(135deg, #4a90e2 0%, #00bcd4 100%)',
dangerColor: '#f44336',
successColor: '#4caf50'
},
terminal: {
name: 'Terminal',
modalBg: 'rgba(5, 10, 5, 0.98)',
modalHeaderBg: 'rgba(10, 15, 10, 0.95)',
modalFooterBg: 'rgba(8, 12, 8, 0.9)',
sectionBg: 'rgba(12, 20, 12, 0.8)',
inputBg: 'rgba(20, 30, 20, 0.9)',
textColor: '#00ff00',
textSecondary: '#66ff66',
borderColor: 'rgba(0, 255, 0, 0.3)',
mainBtnBg: 'rgba(10, 20, 10, 0.95)',
mainBtnBorder: 'rgba(0, 255, 0, 0.4)',
primaryColor: '#00ff66',
accentGradient: 'linear-gradient(135deg, #00cc00 0%, #00ff99 100%)',
dangerColor: '#ff0033',
successColor: '#33ff00'
}
};
function getThemeNames() {
return Object.keys(THEMES);
}
function getTheme(themeName) {
return THEMES[themeName] || THEMES.torn;
}
// ==================== DEFAULT DATA ====================
const DEFAULT_LINKS = [
{ name: 'Home', url: '/index.php', icon: '🏠', color: '#4aa3df' },
{ name: 'Items', url: '/item.php', icon: '🎒', color: '#3ea34a' },
{ name: 'City', url: '/city.php', icon: '🏙️', color: '#66baff' },
{ name: 'Job', url: '/job.php', icon: '💼', color: '#3ea34a' },
{ name: 'Gym', url: '/gym.php', icon: '💪', color: '#a33a3a' },
{ name: 'Crimes', url: '/crimes.php', icon: '🔫', color: '#a33a3a' },
{ name: 'Missions', url: '/loader.php?sid=missions', icon: '🎯', color: '#4aa3df' },
{ name: 'Newspaper', url: '/newspaper.php', icon: '📰', color: '#9b9b9b' }
];
const DEFAULT_LOADOUTS = {
'default': { name: 'Default', links: [...DEFAULT_LINKS] },
'trading': {
name: 'Trading',
links: [
{ name: 'Bazaar', url: '/bazaar.php', icon: '🏪', color: '#3ea34a' },
{ name: 'Item Market', url: '/imarket.php', icon: '💰', color: '#3ea34a' },
{ name: 'Points Market', url: '/pmarket.php', icon: '⭐', color: '#FFD700' },
{ name: 'Auctions', url: '/auctions.php', icon: '🔨', color: '#4aa3df' },
{ name: 'Trade', url: '/trade.php', icon: '🤝', color: '#4aa3df' },
{ name: 'Display Case', url: '/displaycase.php', icon: '🏆', color: '#66baff' },
{ name: 'Items', url: '/item.php', icon: '🎒', color: '#3ea34a' },
{ name: 'Properties', url: '/properties.php', icon: '🏘️', color: '#9b9b9b' }
]
},
'combat': {
name: 'Combat',
links: [
{ name: 'Gym', url: '/gym.php', icon: '💪', color: '#a33a3a' },
{ name: 'Crimes', url: '/crimes.php', icon: '🔫', color: '#a33a3a' },
{ name: 'Hospital', url: '/hospital.php', icon: '🏥', color: '#a33a3a' },
{ name: 'Faction', url: '/factions.php', icon: '⚔️', color: '#66baff' },
{ name: 'War', url: '/war.php', icon: '💣', color: '#a33a3a' },
{ name: 'Bounties', url: '/bounties.php', icon: '💀', color: '#9b9b9b' },
{ name: 'Attacks', url: '/attacks.php', icon: '⚡', color: '#a33a3a' },
{ name: 'Attack Log', url: '/attacklog.php', icon: '📜', color: '#9b9b9b' }
]
},
'travel': {
name: 'Travel',
links: [
{ name: 'Travel', url: '/travel.php', icon: '✈️', color: '#4aa3df' },
{ name: 'Home', url: '/index.php', icon: '🏠', color: '#4aa3df' },
{ name: 'Items', url: '/item.php', icon: '🎒', color: '#3ea34a' },
{ name: 'Bazaar', url: '/bazaar.php', icon: '🏪', color: '#3ea34a' },
{ name: 'City', url: '/city.php', icon: '🏙️', color: '#66baff' },
{ name: 'Laptop', url: '/pc.php', icon: '💻', color: '#9b9b9b' }
]
},
'social': {
name: 'Social',
links: [
{ name: 'Messages', url: '/messages.php', icon: '💬', color: '#4aa3df' },
{ name: 'Events', url: '/events.php', icon: '📅', color: '#66baff' },
{ name: 'Forums', url: '/forums.php', icon: '💭', color: '#9b9b9b' },
{ name: 'Friends', url: '/friends.php', icon: '👥', color: '#3ea34a' },
{ name: 'Faction', url: '/factions.php', icon: '⚔️', color: '#66baff' },
{ name: 'Company', url: '/companies.php', icon: '🏢', color: '#9b9b9b' }
]
}
};
// ==================== STATE MANAGEMENT ====================
let settings = Storage.get('tornRadialSettings', {
layout: 'circular',
iconSize: 'medium',
currentLoadout: 'default',
theme: 'torn',
notifications: { enabled: true },
screenCalibration: null,
screenCalibrationMobile: null,
apiKey: ''
});
settings = {
layout: settings.layout || 'circular',
iconSize: settings.iconSize || 'medium',
currentLoadout: settings.currentLoadout || 'default',
theme: settings.theme || 'torn',
notifications: settings.notifications || { enabled: true },
screenCalibration: settings.screenCalibration || null,
screenCalibrationMobile: settings.screenCalibrationMobile || null,
apiKey: settings.apiKey || ''
};
if (settings.apiKey) {
API.setApiKey(settings.apiKey);
}
let loadouts = Storage.get('tornRadialLoadouts', DEFAULT_LOADOUTS);
let usageStats = Storage.get('tornRadialUsageStats', {});
let timers = Storage.get('tornRadialTimers', []);
if (!loadouts || typeof loadouts !== 'object') {
ErrorLogger.log('error', 'Invalid loadouts structure, restoring defaults');
loadouts = { ...DEFAULT_LOADOUTS };
}
if (!loadouts[settings.currentLoadout]) {
ErrorLogger.log('error', `Loadout ${settings.currentLoadout} not found, switching to default`);
settings.currentLoadout = 'default';
if (!loadouts['default']) {
loadouts['default'] = DEFAULT_LOADOUTS['default'];
}
}
// Generate favorites
function generateFavoritesLoadout() {
const sorted = Object.entries(usageStats)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([url]) => {
for (const loadout of Object.values(loadouts)) {
const link = loadout.links?.find(l => l.url === url);
if (link) return link;
}
return null;
})
.filter(Boolean);
if (sorted.length > 0) {
loadouts['favorites'] = {
name: '⭐ Favorites',
links: sorted
};
}
}
generateFavoritesLoadout();
let links = [];
try {
links = loadouts[settings.currentLoadout]?.links || [...DEFAULT_LINKS];
links = links.map((link, i) => ({
name: link?.name || 'Unnamed',
url: link?.url || '/index.php',
icon: link?.icon || '🔗',
color: link?.color || DEFAULT_LINKS[i]?.color || '#4aa3df'
}));
} catch (e) {
ErrorLogger.log('error', 'Error loading links from loadout', e);
links = [...DEFAULT_LINKS];
}
let isOpen = false;
let isDragging = false;
let isAnimating = false;
let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
let startX, startY, hasMoved = false;
let calibrationMode = false;
let calibrationStep = 0;
let selectedSearchIndex = 0;
// ==================== POSITION MANAGEMENT ====================
function getSafeInitialPosition() {
const padding = 100;
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
return {
x: Math.max(padding, Math.min(centerX, window.innerWidth - padding)),
y: Math.max(padding, Math.min(centerY, window.innerHeight - padding))
};
}
// Load position based on device type
const positionKey = isPDA ? 'tornRadialPositionMobile' : 'tornRadialPosition';
const savedPos = Storage.get(positionKey, getSafeInitialPosition());
// ==================== SIZE CONFIGURATIONS ====================
const sizeConfig = {
pda: { main: 32, radial: 28, fontSize: 14, radialFont: 12, radius: 65, spacing: 14, maxPerRow: 7 },
small: { main: 52, radial: 42, fontSize: 20, radialFont: 18, radius: 90, spacing: 16, maxPerRow: 9 },
medium: { main: 64, radial: 52, fontSize: 28, radialFont: 24, radius: 110, spacing: 16, maxPerRow: 10 },
large: { main: 76, radial: 62, fontSize: 34, radialFont: 28, radius: 130, spacing: 16, maxPerRow: 12 }
};
let currentSize = sizeConfig[settings.iconSize] || sizeConfig.medium;
const currentTheme = getTheme(settings.theme);
// ==================== HELPER FUNCTIONS ====================
function adjustBrightness(color, percent) {
if (!color || typeof color !== 'string' || !color.startsWith('#')) return '#4aa3df';
try {
const num = parseInt(color.replace('#', ''), 16);
if (isNaN(num)) return '#4aa3df';
const amt = Math.round(2.55 * percent);
const R = Math.min(255, Math.max(0, (num >> 16) + amt));
const G = Math.min(255, Math.max(0, (num >> 8 & 0x00FF) + amt));
const B = Math.min(255, Math.max(0, (num & 0x0000FF) + amt));
return '#' + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1);
} catch(e) {
ErrorLogger.log('error', 'Color adjustment failed', e);
return '#4aa3df';
}
}
function showNotification(message, duration = 3000) {
if (!NotificationManager.canShow(message)) {
return;
}
try {
const notification = document.createElement('div');
notification.className = 'torn-radial-notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.transition = 'all 0.3s ease-out';
notification.style.opacity = '0';
notification.style.transform = 'translateX(400px)';
setTimeout(() => notification.remove(), 300);
}, duration);
} catch(e) {
ErrorLogger.log('error', 'Failed to show notification', e);
}
}
function trackUsage(url) {
try {
if (!usageStats[url]) usageStats[url] = 0;
usageStats[url]++;
Storage.set('tornRadialUsageStats', usageStats);
if (Object.keys(usageStats).length >= 8) {
generateFavoritesLoadout();
Storage.set('tornRadialLoadouts', loadouts);
}
} catch(e) {
ErrorLogger.log('error', 'Failed to track usage', e);
}
}
function calculatePosition(index, total, layout) {
try {
const radius = currentSize?.radius || 110;
const spacing = currentSize?.spacing || 16;
const maxPerRow = currentSize?.maxPerRow || 8;
let x = 0, y = 0;
switch(layout) {
case 'horizontal': {
const isLeftSide = (savedPos?.x || 50) < window.innerWidth / 2;
const isTopHalf = (savedPos?.y || 50) < window.innerHeight / 2;
const row = Math.floor(index / maxPerRow);
const posInRow = index % maxPerRow;
x = isLeftSide ?
(posInRow + 1) * (currentSize.radial + spacing) :
-(posInRow + 1) * (currentSize.radial + spacing);
const rowOffset = (currentSize.radial + spacing) * 0.8;
y = isTopHalf ?
row * rowOffset :
-row * rowOffset;
break;
}
case 'vertical': {
const isTopHalf = (savedPos?.y || 50) < window.innerHeight / 2;
const isLeftSide = (savedPos?.x || 50) < window.innerWidth / 2;
const col = Math.floor(index / maxPerRow);
const posInCol = index % maxPerRow;
y = isTopHalf ?
(posInCol + 1) * (currentSize.radial + spacing) :
-(posInCol + 1) * (currentSize.radial + spacing);
const colOffset = (currentSize.radial + spacing) * 0.8;
x = isLeftSide ?
col * colOffset :
-col * colOffset;
break;
}
case 'circular':
default: {
const maxPerRing = 12;
const ring = Math.floor(index / maxPerRing);
const posInRing = index % maxPerRing;
const totalInRing = Math.min(maxPerRing, total - (ring * maxPerRing));
const ringRadius = radius + (ring * (currentSize.radial + spacing));
const angleStep = (2 * Math.PI) / totalInRing;
const angle = angleStep * posInRing - Math.PI / 2;
x = Math.cos(angle) * ringRadius;
y = Math.sin(angle) * ringRadius;
break;
}
}
return { x, y };
} catch(e) {
ErrorLogger.log('error', 'Position calculation failed', e);
return { x: 0, y: 0 };
}
}
// Enhanced bounds checking with device-specific calibration
function checkBounds(pos) {
try {
const menuX = savedPos.x;
const menuY = savedPos.y;
const buttonRadius = currentSize.radial / 2;
const itemX = menuX + pos.x;
const itemY = menuY + pos.y;
// Get calibration based on device type
const calibration = isPDA ? settings.screenCalibrationMobile : settings.screenCalibration;
let bounds;
if (calibration) {
// STRICT: Use exact calibration points
bounds = {
left: calibration.topLeft.x + buttonRadius,
right: calibration.bottomRight.x - buttonRadius,
top: calibration.topLeft.y + buttonRadius,
bottom: calibration.bottomRight.y - buttonRadius
};
log('Calibration bounds:', bounds);
log('Item position:', itemX, itemY);
} else {
bounds = {
left: 10 + buttonRadius,
right: window.innerWidth - 90 - buttonRadius,
top: 25 + buttonRadius,
bottom: window.innerHeight - 25 - buttonRadius
};
}
let needsReposition = false;
let newX = menuX;
let newY = menuY;
// Check if ANY item exceeds bounds
if (itemX < bounds.left) {
newX = menuX + (bounds.left - itemX);
needsReposition = true;
log('Item exceeds left bound, adjusting');
} else if (itemX > bounds.right) {
newX = menuX - (itemX - bounds.right);
needsReposition = true;
log('Item exceeds right bound, adjusting');
}
if (itemY < bounds.top) {
newY = menuY + (bounds.top - itemY);
needsReposition = true;
log('Item exceeds top bound, adjusting');
} else if (itemY > bounds.bottom) {
newY = menuY - (itemY - bounds.bottom);
needsReposition = true;
log('Item exceeds bottom bound, adjusting');
}
if (needsReposition) {
savedPos.x = Math.round(newX);
savedPos.y = Math.round(newY);
container.style.left = savedPos.x + 'px';
container.style.top = savedPos.y + 'px';
Storage.set(positionKey, savedPos);
showNotification('⚠️ Menu repositioned to stay within bounds');
setTimeout(() => {
createRadialItems();
}, 100);
}
} catch(e) {
ErrorLogger.log('error', 'Failed to check bounds', e);
}
}
// PDA responsive scaling
function getResponsiveSize(baseSize) {
if (isPDA) {
return Math.round(baseSize * 0.8);
}
return baseSize;
}
function getResponsiveFontSize(baseFontSize) {
if (isPDA) {
return Math.round(baseFontSize * 0.85);
}
return baseFontSize;
}
// ==================== CSS INJECTION ====================
const style = document.createElement('style');
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&display=swap');
* {
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', 'Segoe UI', sans-serif;
}
#torn-radial-container {
position: fixed;
left: ${savedPos.x}px;
top: ${savedPos.y}px;
z-index: 999999;
pointer-events: none;
transform-origin: center center;
}
#torn-radial-btn {
width: ${currentSize.main}px;
height: ${currentSize.main}px;
border-radius: 50%;
background: ${currentTheme.mainBtnBg};
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border: 2px solid ${currentTheme.mainBtnBorder};
cursor: pointer;
pointer-events: auto;
box-shadow:
0 12px 48px rgba(0, 0, 0, 0.15),
0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex;
align-items: center;
justify-content: center;
font-size: ${currentSize.fontSize}px;
user-select: none;
position: relative;
color: ${currentTheme.primaryColor};
}
#torn-radial-btn:active {
transform: scale(0.92);
}
#torn-radial-btn:hover {
transform: scale(1.05);
box-shadow:
0 16px 56px rgba(0, 0, 0, 0.2),
0 4px 12px rgba(0, 0, 0, 0.1),
0 0 0 4px ${currentTheme.primaryColor}33;
animation: pulse-glow 2s infinite;
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 16px 56px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 4px ${currentTheme.primaryColor}33; }
50% { box-shadow: 0 16px 56px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 8px ${currentTheme.primaryColor}55; }
}
#torn-radial-btn.dragging {
cursor: grabbing;
transform: scale(1.1);
box-shadow:
0 20px 64px rgba(0, 0, 0, 0.25),
0 6px 16px rgba(0, 0, 0, 0.15);
}
.radial-item {
position: absolute;
width: ${currentSize.radial}px;
height: ${currentSize.radial}px;
border-radius: 50%;
cursor: pointer;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: ${currentSize.radialFont}px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.2),
0 2px 6px rgba(0, 0, 0, 0.12);
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
opacity: 0;
transform: scale(0);
text-decoration: none;
left: ${(currentSize.main - currentSize.radial) / 2}px;
top: ${(currentSize.main - currentSize.radial) / 2}px;
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
will-change: transform, opacity;
}
.radial-item::before {
content: attr(title);
position: absolute;
bottom: -36px;
left: 50%;
transform: translateX(-50%) scale(0);
background: rgba(0, 0, 0, 0.92);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
color: white;
padding: 6px 12px;
border-radius: 8px;
font-size: ${isPDA ? '10px' : '12px'};
font-weight: 600;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
letter-spacing: 0.3px;
z-index: 100;
}
.radial-item:hover::before {
opacity: 1;
transform: translateX(-50%) scale(1);
bottom: -40px;
}
.radial-item:active {
transform: scale(0.85) !important;
}
.radial-item:hover {
transform: scale(1.15) !important;
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.25),
0 4px 8px rgba(0, 0, 0, 0.15);
z-index: 10;
}
.radial-item.open {
opacity: 1;
}
.radial-item.settings {
background: linear-gradient(135deg, #8E8E93 0%, #636366 100%);
}
.radial-item.search-icon {
background: ${currentTheme.accentGradient};
}
.radial-item.calculator {
background: linear-gradient(135deg, #FF9500 0%, #FF6B00 100%);
}
.radial-item.mini-apps {
background: linear-gradient(135deg, ${currentTheme.successColor} 0%, #2FB350 100%);
}
/* Search Overlay */
#torn-radial-search-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(20px);
z-index: 1000001;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
}
#torn-radial-search-overlay.show {
display: flex;
}
.search-container {
background: ${currentTheme.modalBg};
border-radius: ${isPDA ? '16px' : '20px'};
padding: ${isPDA ? '20px' : '30px'};
max-width: ${isPDA ? '95%' : '700px'};
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5);
}
.radial-search-container h2 {
margin: 0 0 ${isPDA ? '16px' : '20px'} 0;
color: ${currentTheme.textColor};
font-size: ${isPDA ? '18px' : '24px'};
}
.search-input-wrapper {
position: relative;
margin-bottom: ${isPDA ? '16px' : '20px'};
}
.search-input {
width: 100%;
padding: ${isPDA ? '12px 16px' : '16px 20px'};
border: 2px solid ${currentTheme.borderColor};
border-radius: ${isPDA ? '10px' : '12px'};
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
font-size: ${isPDA ? '14px' : '16px'};
transition: all 0.2s ease;
}
.radial-search-input:focus {
outline: none;
border-color: ${currentTheme.primaryColor};
box-shadow: 0 0 0 4px ${currentTheme.primaryColor}26;
}
.search-results {
max-height: 400px;
overflow-y: auto;
}
.search-category {
margin: ${isPDA ? '16px 0' : '20px 0'};
}
.search-category-title {
font-size: ${isPDA ? '12px' : '14px'};
font-weight: 600;
color: ${currentTheme.textSecondary};
text-transform: uppercase;
margin-bottom: 8px;
letter-spacing: 0.5px;
}
.search-result-item {
padding: ${isPDA ? '10px 12px' : '12px 16px'};
background: ${currentTheme.sectionBg};
margin: 8px 0;
border-radius: ${isPDA ? '8px' : '10px'};
cursor: pointer;
transition: all 0.2s ease;
color: ${currentTheme.textColor};
display: flex;
align-items: center;
gap: 12px;
font-size: ${isPDA ? '13px' : '14px'};
}
.radial-search-result-item.selected {
background: ${currentTheme.primaryColor};
color: white;
}
.radial-search-result-item:hover {
background: ${currentTheme.primaryColor};
color: white;
transform: translateX(4px);
}
.radial-search-history {
margin-top: ${isPDA ? '16px' : '20px'};
padding-top: ${isPDA ? '16px' : '20px'};
border-top: 1px solid ${currentTheme.borderColor};
}
.search-history-title {
font-size: ${isPDA ? '11px' : '12px'};
color: ${currentTheme.textSecondary};
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-clear-history {
background: none;
border: none;
color: ${currentTheme.dangerColor};
cursor: pointer;
font-size: ${isPDA ? '10px' : '11px'};
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
.radial-search-clear-history:hover {
background: rgba(163, 58, 58, 0.2);
}
.search-no-results {
text-align: center;
padding: ${isPDA ? '30px 16px' : '40px 20px'};
color: ${currentTheme.textSecondary};
font-size: ${isPDA ? '13px' : '14px'};
}
/* Calculator */
#torn-radial-calculator {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(20px);
z-index: 1000001;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
padding: 20px;
}
#torn-radial-calculator.show {
display: flex;
}
.calculator-container {
background: ${currentTheme.modalBg};
border-radius: ${isPDA ? '16px' : '20px'};
padding: 0;
max-width: ${isPDA ? '95%' : '400px'};
width: 100%;
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.calculator-header {
padding: ${isPDA ? '16px 20px' : '20px 24px'};
border-bottom: 1px solid ${currentTheme.borderColor};
background: ${currentTheme.modalHeaderBg};
display: flex;
justify-content: space-between;
align-items: center;
}
.calculator-header h2 {
margin: 0;
color: ${currentTheme.textColor};
font-size: ${isPDA ? '16px' : '20px'};
}
.calculator-display {
padding: ${isPDA ? '20px 16px' : '30px 20px'};
background: ${currentTheme.sectionBg};
text-align: right;
font-size: ${isPDA ? '28px' : '36px'};
font-weight: 300;
color: ${currentTheme.textColor};
min-height: ${isPDA ? '60px' : '80px'};
word-break: break-all;
font-family: 'Courier New', monospace;
}
.calculator-buttons {
padding: ${isPDA ? '16px' : '20px'};
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: ${isPDA ? '8px' : '12px'};
}
.calc-btn {
padding: ${isPDA ? '16px' : '20px'};
border: none;
border-radius: ${isPDA ? '10px' : '12px'};
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
font-size: ${isPDA ? '16px' : '18px'};
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.calc-btn:hover {
background: ${currentTheme.borderColor};
transform: scale(1.05);
}
.calc-btn:active {
transform: scale(0.95);
}
.calc-btn.operator {
background: ${currentTheme.primaryColor};
color: white;
}
.calc-btn.equals {
background: ${currentTheme.successColor};
color: white;
grid-column: span 2;
}
.calc-btn.clear {
background: ${currentTheme.dangerColor};
color: white;
}
/* Mini Apps Overlay */
#torn-radial-mini-apps {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(20px);
z-index: 1000001;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
padding: 20px;
overflow-y: auto;
}
#torn-radial-mini-apps.show {
display: flex;
}
.mini-apps-container {
background: ${currentTheme.modalBg};
border-radius: ${isPDA ? '16px' : '20px'};
padding: 0;
max-width: ${isPDA ? '95%' : '800px'};
width: 100%;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5);
}
.mini-apps-header {
padding: ${isPDA ? '16px 20px' : '20px 24px'};
border-bottom: 1px solid ${currentTheme.borderColor};
background: ${currentTheme.modalHeaderBg};
display: flex;
justify-content: space-between;
align-items: center;
}
.mini-apps-header h2 {
margin: 0;
color: ${currentTheme.textColor};
font-size: ${isPDA ? '18px' : '22px'};
}
.mini-apps-body {
padding: ${isPDA ? '16px' : '24px'};
overflow-y: auto;
flex: 1;
}
.mini-app-section {
background: ${currentTheme.sectionBg};
padding: ${isPDA ? '16px' : '20px'};
margin-bottom: ${isPDA ? '16px' : '20px'};
border-radius: ${isPDA ? '12px' : '14px'};
border: 1px solid ${currentTheme.borderColor};
}
.mini-app-section h3 {
margin: 0 0 ${isPDA ? '12px' : '16px'} 0;
color: ${currentTheme.textColor};
font-size: ${isPDA ? '16px' : '18px'};
}
.timer-item {
background: ${currentTheme.inputBg};
padding: ${isPDA ? '10px 12px' : '12px 16px'};
margin: 8px 0;
border-radius: ${isPDA ? '8px' : '10px'};
display: flex;
justify-content: space-between;
align-items: center;
color: ${currentTheme.textColor};
font-size: ${isPDA ? '13px' : '14px'};
}
.timer-controls {
display: flex;
gap: 8px;
}
.timer-btn {
padding: ${isPDA ? '5px 10px' : '6px 12px'};
border: none;
border-radius: ${isPDA ? '6px' : '8px'};
background: ${currentTheme.primaryColor};
color: white;
font-size: ${isPDA ? '11px' : '12px'};
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.timer-btn:hover {
opacity: 0.8;
}
.timer-btn.danger {
background: ${currentTheme.dangerColor};
}
/* API Monitor Section */
.api-bars {
display: grid;
grid-template-columns: repeat(${isPDA ? '1' : '2'}, 1fr);
gap: ${isPDA ? '10px' : '12px'};
}
.api-bar {
background: ${currentTheme.inputBg};
padding: ${isPDA ? '10px' : '12px'};
border-radius: ${isPDA ? '8px' : '10px'};
}
.api-bar-label {
font-size: ${isPDA ? '11px' : '12px'};
color: ${currentTheme.textSecondary};
margin-bottom: 4px;
}
.api-bar-value {
font-size: ${isPDA ? '16px' : '18px'};
font-weight: 600;
color: ${currentTheme.textColor};
}
.api-bar-progress {
height: 4px;
background: ${currentTheme.borderColor};
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.api-bar-progress-fill {
height: 100%;
background: ${currentTheme.primaryColor};
transition: width 0.3s ease;
}
/* Calibration Overlay */
#torn-radial-calibration {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 1000002;
cursor: crosshair;
}
#torn-radial-calibration.show {
display: block;
}
.calibration-instruction {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${currentTheme.modalBg};
padding: ${isPDA ? '20px 30px' : '30px 40px'};
border-radius: ${isPDA ? '16px' : '20px'};
color: ${currentTheme.textColor};
font-size: ${isPDA ? '18px' : '24px'};
font-weight: 600;
text-align: center;
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5);
pointer-events: none;
max-width: ${isPDA ? '80%' : 'none'};
}
.calibration-point {
position: absolute;
width: ${isPDA ? '16px' : '20px'};
height: ${isPDA ? '16px' : '20px'};
background: ${currentTheme.primaryColor};
border: 3px solid white;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
50% { transform: translate(-50%, -50%) scale(1.3); opacity: 0.7; }
}
.calibration-cancel {
position: fixed;
bottom: ${isPDA ? '20px' : '40px'};
left: 50%;
transform: translateX(-50%);
padding: ${isPDA ? '10px 20px' : '12px 24px'};
background: ${currentTheme.dangerColor};
color: white;
border: none;
border-radius: ${isPDA ? '10px' : '12px'};
font-size: ${isPDA ? '14px' : '16px'};
font-weight: 600;
cursor: pointer;
pointer-events: auto;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
/* Settings Modal - Tabbed Interface */
#torn-radial-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
z-index: 1000000;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
padding: ${isPDA ? '10px' : '20px'};
overflow-y: auto;
}
#torn-radial-modal.show {
display: flex;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-content {
background: ${currentTheme.modalBg};
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-radius: ${isPDA ? '16px' : '20px'};
padding: 0;
max-width: ${isPDA ? '100%' : '800px'};
width: 100%;
max-height: ${isPDA ? '95vh' : '90vh'};
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow:
0 32px 64px rgba(0, 0, 0, 0.25),
0 0 0 0.5px ${currentTheme.borderColor};
color: ${currentTheme.textColor};
animation: slideUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
margin: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: ${isPDA ? '12px 16px' : '16px 24px'};
border-bottom: 0.5px solid ${currentTheme.borderColor};
background: ${currentTheme.modalHeaderBg};
flex-shrink: 0;
}
.modal-header-title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.modal-content h2 {
margin: 0;
color: ${currentTheme.textColor};
font-size: ${isPDA ? '16px' : '20px'};
font-weight: 700;
letter-spacing: -0.3px;
}
.error-log-btn {
width: ${isPDA ? '26px' : '32px'};
height: ${isPDA ? '26px' : '32px'};
border-radius: 50%;
background: ${currentTheme.inputBg};
border: none;
font-size: ${isPDA ? '13px' : '16px'};
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
margin-right: 8px;
}
.error-log-btn:hover {
background: rgba(255, 152, 0, 0.2);
transform: scale(1.05);
}
.modal-close {
width: ${isPDA ? '26px' : '32px'};
height: ${isPDA ? '26px' : '32px'};
border-radius: 50%;
background: ${currentTheme.inputBg};
border: none;
color: ${currentTheme.textSecondary};
font-size: ${isPDA ? '16px' : '20px'};
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.modal-close:hover {
background: ${currentTheme.dangerColor};
color: white;
}
.modal-close:active {
transform: scale(0.9);
}
/* Tab Navigation */
.modal-tabs {
display: flex;
padding: ${isPDA ? '8px 12px' : '12px 16px'};
background: ${currentTheme.modalHeaderBg};
border-bottom: 1px solid ${currentTheme.borderColor};
gap: ${isPDA ? '4px' : '8px'};
overflow-x: auto;
flex-shrink: 0;
}
.modal-tab {
padding: ${isPDA ? '6px 12px' : '8px 16px'};
border: none;
border-radius: ${isPDA ? '8px' : '10px'};
background: transparent;
color: ${currentTheme.textSecondary};
font-size: ${isPDA ? '12px' : '14px'};
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
flex-shrink: 0;
}
.modal-tab:hover {
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
}
.modal-tab.active {
background: ${currentTheme.primaryColor};
color: white;
box-shadow: 0 2px 8px ${currentTheme.primaryColor}33;
}
.modal-body {
padding: ${isPDA ? '12px 16px' : '20px 24px'};
overflow-y: auto;
flex: 1;
min-height: 0;
}
.modal-body::-webkit-scrollbar {
width: 6px;
}
.modal-body::-webkit-scrollbar-track {
background: transparent;
}
.modal-body::-webkit-scrollbar-thumb {
background: rgba(74, 163, 223, 0.3);
border-radius: 10px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
.loadout-selector {
background: ${currentTheme.sectionBg};
padding: ${isPDA ? '12px' : '16px'};
margin-bottom: ${isPDA ? '12px' : '16px'};
border-radius: ${isPDA ? '12px' : '14px'};
border: 0.5px solid ${currentTheme.borderColor};
}
.loadout-selector h3 {
margin: 0 0 ${isPDA ? '10px' : '12px'} 0;
font-size: ${isPDA ? '14px' : '16px'};
font-weight: 600;
color: ${currentTheme.textColor};
}
.loadout-tabs {
display: flex;
gap: ${isPDA ? '6px' : '8px'};
flex-wrap: wrap;
margin-bottom: ${isPDA ? '10px' : '12px'};
}
.loadout-tab {
padding: ${isPDA ? '6px 10px' : '8px 14px'};
border: none;
border-radius: ${isPDA ? '8px' : '10px'};
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
font-size: ${isPDA ? '11px' : '13px'};
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.loadout-tab.active {
background: ${currentTheme.primaryColor};
color: white;
box-shadow: 0 2px 8px ${currentTheme.primaryColor}33;
}
.loadout-tab:hover:not(.active) {
background: ${currentTheme.borderColor};
}
.loadout-actions {
display: grid;
grid-template-columns: repeat(${isPDA ? '1' : '3'}, 1fr);
gap: ${isPDA ? '6px' : '8px'};
margin-top: ${isPDA ? '10px' : '12px'};
}
.loadout-actions button {
padding: ${isPDA ? '8px 12px' : '10px 16px'};
border: none;
border-radius: ${isPDA ? '8px' : '10px'};
background: rgba(62, 163, 74, 0.2);
color: ${currentTheme.successColor};
font-size: ${isPDA ? '11px' : '13px'};
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.loadout-actions button:hover {
background: rgba(62, 163, 74, 0.3);
}
.loadout-actions button.delete {
background: rgba(163, 58, 58, 0.2);
color: ${currentTheme.dangerColor};
}
.loadout-actions button.delete:hover {
background: rgba(163, 58, 58, 0.3);
}
.settings-section {
background: ${currentTheme.sectionBg};
padding: ${isPDA ? '12px' : '16px'};
margin-bottom: ${isPDA ? '12px' : '16px'};
border-radius: ${isPDA ? '12px' : '14px'};
border: 0.5px solid ${currentTheme.borderColor};
}
.settings-section h3 {
margin: 0 0 ${isPDA ? '10px' : '14px'} 0;
font-size: ${isPDA ? '14px' : '16px'};
font-weight: 600;
color: ${currentTheme.textColor};
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: ${isPDA ? '8px 0' : '12px 0'};
gap: ${isPDA ? '8px' : '12px'};
flex-wrap: ${isPDA ? 'wrap' : 'nowrap'};
}
.setting-item label {
font-size: ${isPDA ? '12px' : '14px'};
font-weight: 500;
color: ${currentTheme.textColor};
flex: ${isPDA ? '1 1 100%' : '1'};
}
.setting-item select,
.setting-item input[type="text"],
.setting-item input[type="password"] {
padding: ${isPDA ? '8px 10px' : '10px 12px'};
border: none;
border-radius: ${isPDA ? '8px' : '10px'};
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
font-size: ${isPDA ? '12px' : '14px'};
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
min-width: ${isPDA ? '100%' : '140px'};
}
.setting-item input[type="checkbox"] {
width: ${isPDA ? '18px' : '20px'};
height: ${isPDA ? '18px' : '20px'};
min-width: ${isPDA ? '18px' : '20px'};
cursor: pointer;
}
.setting-item select:focus,
.setting-item input[type="text"]:focus,
.setting-item input[type="password"]:focus {
outline: none;
border: 2px solid ${currentTheme.primaryColor};
box-shadow: 0 0 0 4px ${currentTheme.primaryColor}26;
}
.link-item {
background: ${currentTheme.sectionBg};
padding: ${isPDA ? '10px' : '14px'};
margin: ${isPDA ? '8px 0' : '12px 0'};
border-radius: ${isPDA ? '12px' : '14px'};
display: grid;
grid-template-columns: ${isPDA ? '36px 1fr' : '50px 1fr 1fr 50px 40px 80px'};
gap: ${isPDA ? '8px' : '10px'};
align-items: center;
border: 0.5px solid ${currentTheme.borderColor};
transition: all 0.2s ease;
}
.link-item:hover {
background: ${currentTheme.inputBg};
transform: translateX(2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.link-item input {
padding: ${isPDA ? '8px' : '10px 12px'};
border: none;
border-radius: ${isPDA ? '8px' : '10px'};
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
font-size: ${isPDA ? '12px' : '14px'};
font-weight: 500;
transition: all 0.2s ease;
}
.link-item input:focus {
outline: none;
border: 2px solid ${currentTheme.primaryColor};
box-shadow: 0 0 0 4px ${currentTheme.primaryColor}26;
}
.link-item input:first-child {
text-align: center;
font-size: ${isPDA ? '16px' : '20px'};
padding: ${isPDA ? '8px' : '10px'};
}
${isPDA ? `
.link-item input:nth-child(2),
.link-item input:nth-child(3) {
grid-column: span 2;
}
.link-item .color-picker-wrapper,
.link-item .delete-btn,
.link-item .reorder-controls {
grid-column: span 1;
}
` : ''}
.color-picker-wrapper {
position: relative;
}
.color-picker {
width: ${isPDA ? '36px' : '50px'};
height: ${isPDA ? '34px' : '42px'};
border-radius: ${isPDA ? '8px' : '10px'};
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.color-picker::-webkit-color-swatch-wrapper {
padding: 4px;
}
.color-picker::-webkit-color-swatch {
border: none;
border-radius: ${isPDA ? '6px' : '8px'};
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.delete-btn {
width: ${isPDA ? '34px' : '40px'};
height: ${isPDA ? '34px' : '40px'};
border: none;
border-radius: ${isPDA ? '8px' : '10px'};
cursor: pointer;
background: rgba(163, 58, 58, 0.2);
color: ${currentTheme.dangerColor};
font-size: ${isPDA ? '14px' : '18px'};
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.delete-btn:hover {
background: rgba(163, 58, 58, 0.3);
transform: scale(1.05);
}
.delete-btn:active {
transform: scale(0.95);
}
.reorder-controls {
display: flex;
flex-direction: column;
gap: 4px;
}
.reorder-btn {
width: ${isPDA ? '34px' : '36px'};
height: ${isPDA ? '15px' : '18px'};
border: none;
border-radius: ${isPDA ? '5px' : '6px'};
cursor: pointer;
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
font-size: ${isPDA ? '10px' : '12px'};
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.reorder-btn:hover:not(:disabled) {
background: ${currentTheme.primaryColor};
color: white;
transform: scale(1.05);
}
.reorder-btn:active:not(:disabled) {
transform: scale(0.95);
}
.reorder-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.modal-footer {
padding: ${isPDA ? '10px 12px' : '14px 20px'};
border-top: 0.5px solid ${currentTheme.borderColor};
background: ${currentTheme.modalFooterBg};
display: grid;
grid-template-columns: ${isPDA ? '1fr' : 'repeat(2, 1fr)'};
gap: ${isPDA ? '8px' : '10px'};
flex-shrink: 0;
}
.modal-footer button {
padding: ${isPDA ? '10px 14px' : '12px 18px'};
border: none;
border-radius: ${isPDA ? '10px' : '12px'};
cursor: pointer;
font-size: ${isPDA ? '12px' : '14px'};
font-weight: 600;
transition: all 0.2s ease;
letter-spacing: -0.2px;
}
.modal-footer button:active {
transform: scale(0.96);
}
.btn-primary {
background: ${currentTheme.primaryColor};
color: white;
box-shadow: 0 4px 12px ${currentTheme.primaryColor}4D;
}
.btn-primary:hover {
opacity: 0.9;
box-shadow: 0 6px 16px ${currentTheme.primaryColor}66;
}
.btn-secondary {
background: ${currentTheme.inputBg};
color: ${currentTheme.textColor};
}
.btn-secondary:hover {
opacity: 0.8;
}
.btn-success {
background: ${currentTheme.successColor};
color: white;
box-shadow: 0 4px 12px rgba(62, 163, 74, 0.3);
}
.btn-success:hover {
opacity: 0.9;
box-shadow: 0 6px 16px rgba(62, 163, 74, 0.4);
}
.btn-coffee {
background: ${currentTheme.accentGradient};
color: white;
box-shadow: 0 4px 12px ${currentTheme.primaryColor}33;
grid-column: ${isPDA ? 'span 1' : 'span 2'};
}
.btn-coffee:hover {
opacity: 0.9;
box-shadow: 0 6px 16px ${currentTheme.primaryColor}66;
}
.btn-calibrate {
background: linear-gradient(135deg, #FF9500 0%, #FF6B00 100%);
color: white;
}
.btn-calibrate:hover {
opacity: 0.9;
}
.add-current-page-btn {
background: ${currentTheme.successColor};
color: white;
padding: ${isPDA ? '10px 16px' : '12px 20px'};
border: none;
border-radius: ${isPDA ? '10px' : '12px'};
cursor: pointer;
font-weight: 600;
font-size: ${isPDA ? '12px' : '14px'};
width: 100%;
margin-bottom: ${isPDA ? '10px' : '12px'};
transition: all 0.2s ease;
}
.add-current-page-btn:hover {
opacity: 0.9;
transform: translateY(-2px);
}
/* About Section Styles */
.about-section {
background: ${currentTheme.sectionBg};
padding: ${isPDA ? '16px' : '20px'};
margin-bottom: ${isPDA ? '12px' : '16px'};
border-radius: ${isPDA ? '12px' : '14px'};
border: 0.5px solid ${currentTheme.borderColor};
}
.about-section h3 {
margin: 0 0 ${isPDA ? '12px' : '16px'} 0;
font-size: ${isPDA ? '16px' : '18px'};
font-weight: 600;
color: ${currentTheme.textColor};
}
.about-section p {
margin: ${isPDA ? '8px 0' : '10px 0'};
font-size: ${isPDA ? '12px' : '14px'};
color: ${currentTheme.textSecondary};
line-height: 1.6;
}
.about-section ul {
margin: ${isPDA ? '8px 0' : '12px 0'};
padding-left: ${isPDA ? '20px' : '24px'};
}
.about-section li {
margin: ${isPDA ? '6px 0' : '8px 0'};
font-size: ${isPDA ? '12px' : '14px'};
color: ${currentTheme.textSecondary};
line-height: 1.5;
}
.version-badge {
display: inline-block;
background: ${currentTheme.primaryColor};
color: white;
padding: ${isPDA ? '4px 8px' : '4px 12px'};
border-radius: ${isPDA ? '6px' : '8px'};
font-size: ${isPDA ? '11px' : '12px'};
font-weight: 600;
margin-bottom: ${isPDA ? '8px' : '12px'};
}
/* Donators Section */
.donators-list {
display: grid;
grid-template-columns: repeat(${isPDA ? '1' : '2'}, 1fr);
gap: ${isPDA ? '8px' : '12px'};
margin-top: ${isPDA ? '12px' : '16px'};
}
.donator-item {
background: ${currentTheme.inputBg};
padding: ${isPDA ? '10px 12px' : '12px 16px'};
border-radius: ${isPDA ? '8px' : '10px'};
display: flex;
align-items: center;
gap: ${isPDA ? '8px' : '12px'};
transition: all 0.2s ease;
}
.donator-item:hover {
background: ${currentTheme.borderColor};
transform: translateX(2px);
}
.donator-icon {
font-size: ${isPDA ? '20px' : '24px'};
}
.donator-info {
flex: 1;
}
.donator-name {
font-size: ${isPDA ? '13px' : '14px'};
font-weight: 600;
color: ${currentTheme.textColor};
}
.donator-amount {
font-size: ${isPDA ? '11px' : '12px'};
color: ${currentTheme.textSecondary};
}
/* Notification Toast */
.torn-radial-notification {
position: fixed;
top: ${isPDA ? '10px' : '20px'};
right: ${isPDA ? '10px' : '20px'};
background: ${currentTheme.modalBg};
border: 2px solid ${currentTheme.primaryColor};
color: ${currentTheme.textColor};
padding: ${isPDA ? '12px 16px' : '16px 24px'};
border-radius: ${isPDA ? '10px' : '12px'};
font-size: ${isPDA ? '12px' : '14px'};
font-weight: 600;
z-index: 9999999;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
animation: slideInRight 0.3s ease-out;
max-width: ${isPDA ? '80%' : '400px'};
}
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);
// ==================== CREATE RADIAL ITEMS ====================
const container = document.createElement('div');
container.id = 'torn-radial-container';
document.body.appendChild(container);
const btn = document.createElement('div');
btn.id = 'torn-radial-btn';
btn.innerHTML = '⚡';
btn.title = 'Quick Travel Menu';
container.appendChild(btn);
function createRadialItems() {
try {
const existingItems = document.querySelectorAll('.radial-item');
existingItems.forEach(el => {
try {
el.remove();
} catch(e) {
ErrorLogger.log('warning', 'Failed to remove existing item', e);
}
});
if (!Array.isArray(links) || links.length === 0) {
ErrorLogger.log('error', 'No links available to create');
return;
}
// 3 extra items: search, calculator, mini-apps, settings (search now has icon)
const total = links.length + 4;
// Create link items
links.forEach((link, i) => {
try {
const pos = calculatePosition(i, total, settings.layout);
const item = document.createElement('a');
item.className = 'radial-item';
item.href = link.url || '/index.php';
item.innerHTML = link.icon || '🔗';
item.title = link.name || 'Unnamed';
item.style.background = `linear-gradient(135deg, ${link.color || '#4aa3df'} 0%, ${adjustBrightness(link.color || '#4aa3df', -20)} 100%)`;
item.dataset.x = pos.x;
item.dataset.y = pos.y;
item.addEventListener('click', (e) => {
e.preventDefault();
trackUsage(link.url);
window.location.href = link.url;
});
container.appendChild(item);
checkBounds(pos);
} catch(e) {
ErrorLogger.log('error', `Failed to create link ${i}: ${link.name}`, e);
}
});
// Search button (NOW HAS ITS OWN ICON!)
try {
const pos = calculatePosition(links.length, total, settings.layout);
const searchItem = document.createElement('div');
searchItem.className = 'radial-item search-icon';
searchItem.innerHTML = '👀';
searchItem.title = 'Search';
searchItem.dataset.x = pos.x;
searchItem.dataset.y = pos.y;
searchItem.addEventListener('click', openSearch);
container.appendChild(searchItem);
checkBounds(pos);
} catch(e) {
ErrorLogger.log('error', 'Failed to create search button', e);
}
// Calculator button
try {
const pos = calculatePosition(links.length + 1, total, settings.layout);
const calcItem = document.createElement('div');
calcItem.className = 'radial-item calculator';
calcItem.innerHTML = '🔢';
calcItem.title = 'Calculator';
calcItem.dataset.x = pos.x;
calcItem.dataset.y = pos.y;
calcItem.addEventListener('click', openCalculator);
container.appendChild(calcItem);
checkBounds(pos);
} catch(e) {
ErrorLogger.log('error', 'Failed to create calculator button', e);
}
// Mini-apps button
try {
const pos = calculatePosition(links.length + 2, total, settings.layout);
const miniAppsItem = document.createElement('div');
miniAppsItem.className = 'radial-item mini-apps';
miniAppsItem.innerHTML = '🛠️';
miniAppsItem.title = 'Mini Apps';
miniAppsItem.dataset.x = pos.x;
miniAppsItem.dataset.y = pos.y;
miniAppsItem.addEventListener('click', openMiniApps);
container.appendChild(miniAppsItem);
checkBounds(pos);
} catch(e) {
ErrorLogger.log('error', 'Failed to create mini-apps button', e);
}
// Settings button
try {
const pos = calculatePosition(links.length + 3, total, settings.layout);
const settingsItem = document.createElement('div');
settingsItem.className = 'radial-item settings';
settingsItem.innerHTML = '⚙️';
settingsItem.title = 'Settings';
settingsItem.dataset.x = pos.x;
settingsItem.dataset.y = pos.y;
settingsItem.addEventListener('click', openSettings);
container.appendChild(settingsItem);
checkBounds(pos);
} catch(e) {
ErrorLogger.log('error', 'Failed to create settings button', e);
}
} catch(e) {
ErrorLogger.log('error', 'Fatal error in createRadialItems', e);
}
}
createRadialItems();
// ==================== MENU TOGGLE ====================
function toggleMenu(e) {
if (isDragging || isAnimating) return;
try {
isAnimating = true;
isOpen = !isOpen;
const items = document.querySelectorAll('.radial-item');
if (!items || items.length === 0) {
isAnimating = false;
ErrorLogger.log('error', 'No radial items found to toggle');
return;
}
if (isOpen) {
items.forEach((item, i) => {
setTimeout(() => {
try {
item.classList.add('open');
const x = parseFloat(item.dataset.x) || 0;
const y = parseFloat(item.dataset.y) || 0;
item.style.transform = `translate(${x}px, ${y}px) scale(1)`;
} catch(e) {
ErrorLogger.log('error', `Failed to open item ${i}`, e);
}
}, i * 35);
});
setTimeout(() => { isAnimating = false; }, items.length * 35 + 300);
} else {
items.forEach((item, i) => {
setTimeout(() => {
try {
item.classList.remove('open');
const x = parseFloat(item.dataset.x) || 0;
const y = parseFloat(item.dataset.y) || 0;
item.style.transform = `translate(${x * 0.1}px, ${y * 0.1}px) scale(0.3)`;
setTimeout(() => {
if (!isOpen) {
item.style.transform = 'translate(0, 0) scale(0)';
}
}, 200);
} catch(e) {
ErrorLogger.log('error', `Failed to close item ${i}`, e);
}
}, i * 20);
});
setTimeout(() => { isAnimating = false; }, items.length * 20 + 400);
}
} catch(e) {
ErrorLogger.log('error', 'Failed to toggle menu', e);
isAnimating = false;
}
}
// ==================== DRAG FUNCTIONALITY ====================
function dragStart(e) {
if (e.target !== btn) return;
try {
hasMoved = false;
isDragging = false;
if (e.type === 'touchstart') {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
initialX = e.touches[0].clientX;
initialY = e.touches[0].clientY;
} else {
startX = e.clientX;
startY = e.clientY;
initialX = e.clientX;
initialY = e.clientY;
}
xOffset = parseInt(container.style.left) || savedPos.x;
yOffset = parseInt(container.style.top) || savedPos.y;
document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchend', dragEnd);
} catch(e) {
ErrorLogger.log('error', 'Drag start failed', e);
}
}
function drag(e) {
try {
e.preventDefault();
if (e.type === 'touchmove') {
currentX = e.touches[0].clientX;
currentY = e.touches[0].clientY;
} else {
currentX = e.clientX;
currentY = e.clientY;
}
const deltaX = currentX - startX;
const deltaY = currentY - startY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 5 && !isDragging) {
isDragging = true;
hasMoved = true;
btn.classList.add('dragging');
}
if (isDragging) {
const moveX = currentX - initialX;
const moveY = currentY - initialY;
container.style.left = (xOffset + moveX) + 'px';
container.style.top = (yOffset + moveY) + 'px';
}
} catch(e) {
ErrorLogger.log('error', 'Drag movement failed', e);
}
}
function dragEnd(e) {
try {
document.removeEventListener('mousemove', drag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('mouseup', dragEnd);
document.removeEventListener('touchend', dragEnd);
if (isDragging && hasMoved) {
let newX = parseInt(container.style.left);
let newY = parseInt(container.style.top);
const padding = 5;
const buttonRadius = currentSize.main / 2;
const minX = padding + buttonRadius;
const maxX = window.innerWidth - padding - buttonRadius;
const minY = padding + buttonRadius;
const maxY = window.innerHeight - padding - buttonRadius;
newX = Math.max(minX, Math.min(newX, maxX));
newY = Math.max(minY, Math.min(newY, maxY));
savedPos.x = Math.round(newX);
savedPos.y = Math.round(newY);
container.style.left = savedPos.x + 'px';
container.style.top = savedPos.y + 'px';
Storage.set(positionKey, savedPos);
createRadialItems();
if (isOpen) {
isOpen = false;
setTimeout(() => toggleMenu(), 100);
}
}
btn.classList.remove('dragging');
setTimeout(() => {
isDragging = false;
hasMoved = false;
}, 50);
} catch(e) {
ErrorLogger.log('error', 'Drag end failed', e);
}
}
btn.addEventListener('mousedown', dragStart);
btn.addEventListener('touchstart', dragStart, { passive: false });
btn.addEventListener('click', (e) => {
if (!hasMoved && !isDragging) {
toggleMenu(e);
}
});
// ==================== SEARCH FUNCTIONALITY ====================
const searchOverlay = document.createElement('div');
searchOverlay.id = 'torn-radial-search-overlay';
searchOverlay.innerHTML = `
<div class="search-container">
<h2>🔍 Quick Search</h2>
<div class="search-input-wrapper">
<input type="text" class="search-input" id="torn-search-input" placeholder="Search players, items, pages..." autocomplete="off">
</div>
<div class="search-results" id="torn-search-results"></div>
<div class="search-history" id="torn-search-history" style="display: none;">
<div class="search-history-title">
<span>Recent Searches</span>
<button class="search-clear-history" id="clear-search-history">Clear</button>
</div>
<div id="search-history-items"></div>
</div>
</div>
`;
document.body.appendChild(searchOverlay);
function openSearch() {
if (isOpen) toggleMenu();
searchOverlay.classList.add('show');
setTimeout(() => {
document.getElementById('torn-search-input').focus();
}, 100);
renderSearchHistory();
}
function renderSearchHistory() {
const history = SearchManager.getHistory();
const historySection = document.getElementById('torn-search-history');
const historyItems = document.getElementById('search-history-items');
if (history.length > 0) {
historySection.style.display = 'block';
historyItems.innerHTML = history.slice(0, 5).map(h => `
<div class="search-result-item" onclick="window.location.href='${h.url}'">
${getSearchIcon(h.type)} ${h.query}
</div>
`).join('');
} else {
historySection.style.display = 'none';
}
}
function getSearchIcon(type) {
const icons = {
player: '👤',
item: '🛒',
page: '📄',
faction: '⚔️',
company: '🏢'
};
return icons[type] || '🔗';
}
searchOverlay.addEventListener('click', (e) => {
if (e.target === searchOverlay) {
searchOverlay.classList.remove('show');
}
});
document.getElementById('torn-search-input').addEventListener('input', async (e) => {
const query = e.target.value.trim();
const resultsDiv = document.getElementById('torn-search-results');
if (!query) {
resultsDiv.innerHTML = '';
renderSearchHistory();
selectedSearchIndex = 0;
return;
}
document.getElementById('torn-search-history').style.display = 'none';
try {
const results = await SearchManager.search(query);
let html = '';
let allResults = [];
if (results.players.length > 0) {
html += '<div class="search-category"><div class="search-category-title">Players</div>';
results.players.forEach((r, i) => {
allResults.push(r);
html += `<div class="search-result-item ${i === selectedSearchIndex ? 'selected' : ''}" data-url="${r.url}" data-type="${r.type}">${r.icon} ${r.name}</div>`;
});
html += '</div>';
}
if (results.items.length > 0) {
html += '<div class="search-category"><div class="search-category-title">Items</div>';
results.items.forEach(r => {
allResults.push(r);
html += `<div class="search-result-item" data-url="${r.url}" data-type="${r.type}">${r.icon} ${r.name}</div>`;
});
html += '</div>';
}
if (results.pages.length > 0) {
html += '<div class="search-category"><div class="search-category-title">Pages</div>';
results.pages.forEach(r => {
allResults.push(r);
html += `<div class="search-result-item" data-url="${r.url}" data-type="${r.type}">${r.icon} ${r.name}</div>`;
});
html += '</div>';
}
if (html === '') {
html = '<div class="search-no-results">No results found. Try a different search term.</div>';
}
resultsDiv.innerHTML = html;
// Add click handlers
resultsDiv.querySelectorAll('.search-result-item').forEach(item => {
item.addEventListener('click', () => {
const url = item.dataset.url;
const type = item.dataset.type;
SearchManager.addToHistory(query, url, type);
window.location.href = url;
});
});
} catch(e) {
ErrorLogger.log('error', 'Search failed', e);
resultsDiv.innerHTML = '<div class="search-no-results">Search error. Please try again.</div>';
}
});
// Keyboard navigation for search
document.getElementById('torn-search-input').addEventListener('keydown', (e) => {
const items = document.querySelectorAll('.search-result-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedSearchIndex = Math.min(selectedSearchIndex + 1, items.length - 1);
updateSearchSelection(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedSearchIndex = Math.max(selectedSearchIndex - 1, 0);
updateSearchSelection(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (items[selectedSearchIndex]) {
items[selectedSearchIndex].click();
}
} else if (e.key === 'Escape') {
searchOverlay.classList.remove('show');
}
});
function updateSearchSelection(items) {
items.forEach((item, i) => {
if (i === selectedSearchIndex) {
item.classList.add('selected');
item.scrollIntoView({ block: 'nearest' });
} else {
item.classList.remove('selected');
}
});
}
document.getElementById('clear-search-history').addEventListener('click', () => {
SearchManager.clearHistory();
renderSearchHistory();
showNotification('✅ Search history cleared');
});
// ==================== CALCULATOR FUNCTIONALITY ====================
const calculatorOverlay = document.createElement('div');
calculatorOverlay.id = 'torn-radial-calculator';
calculatorOverlay.innerHTML = `
<div class="calculator-container">
<div class="calculator-header">
<h2>🔢 Calculator</h2>
<button class="modal-close" id="calculator-close">✕</button>
</div>
<div class="calculator-display" id="calc-display">0</div>
<div class="calculator-buttons">
<button class="calc-btn clear" data-value="C">C</button>
<button class="calc-btn operator" data-value="/">/</button>
<button class="calc-btn operator" data-value="*">×</button>
<button class="calc-btn operator" data-value="-">-</button>
<button class="calc-btn" data-value="7">7</button>
<button class="calc-btn" data-value="8">8</button>
<button class="calc-btn" data-value="9">9</button>
<button class="calc-btn operator" data-value="+">+</button>
<button class="calc-btn" data-value="4">4</button>
<button class="calc-btn" data-value="5">5</button>
<button class="calc-btn" data-value="6">6</button>
<button class="calc-btn operator" data-value="%">%</button>
<button class="calc-btn" data-value="1">1</button>
<button class="calc-btn" data-value="2">2</button>
<button class="calc-btn" data-value="3">3</button>
<button class="calc-btn equals" data-value="=">=</button>
<button class="calc-btn" data-value="0" style="grid-column: span 2;">0</button>
<button class="calc-btn" data-value=".">.</button>
</div>
</div>
`;
document.body.appendChild(calculatorOverlay);
let calcCurrentValue = '0';
let calcPreviousValue = null;
let calcOperation = null;
function openCalculator() {
if (isOpen) toggleMenu();
calculatorOverlay.classList.add('show');
calcCurrentValue = '0';
calcPreviousValue = null;
calcOperation = null;
updateCalcDisplay();
}
function updateCalcDisplay() {
document.getElementById('calc-display').textContent = calcCurrentValue;
}
function handleCalcInput(value) {
if (value === 'C') {
calcCurrentValue = '0';
calcPreviousValue = null;
calcOperation = null;
} else if (['+', '-', '*', '/', '%'].includes(value)) {
if (calcPreviousValue !== null && calcOperation !== null) {
calculateResult();
}
calcPreviousValue = parseFloat(calcCurrentValue);
calcOperation = value;
calcCurrentValue = '0';
} else if (value === '=') {
calculateResult();
} else if (value === '.') {
if (!calcCurrentValue.includes('.')) {
calcCurrentValue += '.';
}
} else {
if (calcCurrentValue === '0') {
calcCurrentValue = value;
} else {
calcCurrentValue += value;
}
}
updateCalcDisplay();
}
function calculateResult() {
if (calcPreviousValue === null || calcOperation === null) return;
const current = parseFloat(calcCurrentValue);
const previous = calcPreviousValue;
let result;
switch(calcOperation) {
case '+':
result = previous + current;
break;
case '-':
result = previous - current;
break;
case '*':
result = previous * current;
break;
case '/':
result = previous / current;
break;
case '%':
result = (previous * current) / 100;
break;
}
calcCurrentValue = result.toString();
calcPreviousValue = null;
calcOperation = null;
}
calculatorOverlay.querySelectorAll('.calc-btn').forEach(btn => {
btn.addEventListener('click', () => {
handleCalcInput(btn.dataset.value);
});
});
document.getElementById('calculator-close').addEventListener('click', () => {
calculatorOverlay.classList.remove('show');
});
calculatorOverlay.addEventListener('click', (e) => {
if (e.target === calculatorOverlay) {
calculatorOverlay.classList.remove('show');
}
});
// Keyboard support for calculator
document.addEventListener('keydown', (e) => {
if (calculatorOverlay.classList.contains('show')) {
const key = e.key;
if (/[0-9.]/.test(key) || ['+', '-', '*', '/', '%'].includes(key)) {
e.preventDefault();
handleCalcInput(key === '*' ? '*' : key);
} else if (key === 'Enter' || key === '=') {
e.preventDefault();
handleCalcInput('=');
} else if (key === 'Escape' || key === 'c' || key === 'C') {
e.preventDefault();
handleCalcInput('C');
} else if (key === 'Backspace') {
e.preventDefault();
calcCurrentValue = calcCurrentValue.slice(0, -1) || '0';
updateCalcDisplay();
}
}
});
// ==================== MINI APPS ====================
const miniAppsOverlay = document.createElement('div');
miniAppsOverlay.id = 'torn-radial-mini-apps';
miniAppsOverlay.innerHTML = `
<div class="mini-apps-container">
<div class="mini-apps-header">
<h2>🛠️ Mini Apps</h2>
<button class="modal-close" id="mini-apps-close">✕</button>
</div>
<div class="mini-apps-body">
<div class="mini-app-section">
<h3>⏱️ Timer Manager</h3>
<div id="timers-list"></div>
<div style="display: flex; gap: 8px; margin-top: 12px; flex-wrap: ${isPDA ? 'wrap' : 'nowrap'};">
<input type="text" id="timer-name" placeholder="Timer name" style="flex: 1; padding: 10px; border-radius: 8px; border: none; background: ${currentTheme.inputBg}; color: ${currentTheme.textColor}; min-width: ${isPDA ? '100%' : 'auto'};">
<input type="number" id="timer-minutes" placeholder="Minutes" style="width: ${isPDA ? '100%' : '100px'}; padding: 10px; border-radius: 8px; border: none; background: ${currentTheme.inputBg}; color: ${currentTheme.textColor};">
<button class="timer-btn" id="add-timer-btn" style="padding: 10px 16px;">Add</button>
</div>
</div>
<div class="mini-app-section" id="api-monitor-section">
<h3>📊 API Monitor</h3>
<div id="api-status">
<p style="color: ${currentTheme.textSecondary}; font-size: 12px; margin: 0 0 12px 0;">Configure your API key in settings to enable real-time monitoring</p>
</div>
<div id="api-data-container" style="display: none;">
<!-- Stats Bars -->
<div class="api-bars" id="api-bars">
<div class="api-bar">
<div class="api-bar-label">Energy</div>
<div class="api-bar-value" id="energy-value">-/-</div>
<div class="api-bar-progress">
<div class="api-bar-progress-fill" id="energy-progress" style="width: 0%;"></div>
</div>
<div class="api-bar-label" style="font-size: 10px; margin-top: 4px;" id="energy-time">-</div>
</div>
<div class="api-bar">
<div class="api-bar-label">Nerve</div>
<div class="api-bar-value" id="nerve-value">-/-</div>
<div class="api-bar-progress">
<div class="api-bar-progress-fill" id="nerve-progress" style="width: 0%;"></div>
</div>
<div class="api-bar-label" style="font-size: 10px; margin-top: 4px;" id="nerve-time">-</div>
</div>
<div class="api-bar">
<div class="api-bar-label">Happy</div>
<div class="api-bar-value" id="happy-value">-/-</div>
<div class="api-bar-progress">
<div class="api-bar-progress-fill" id="happy-progress" style="width: 0%;"></div>
</div>
<div class="api-bar-label" style="font-size: 10px; margin-top: 4px;" id="happy-time">-</div>
</div>
<div class="api-bar">
<div class="api-bar-label">Life</div>
<div class="api-bar-value" id="life-value">-/-</div>
<div class="api-bar-progress">
<div class="api-bar-progress-fill" id="life-progress" style="width: 0%;"></div>
</div>
</div>
</div>
<!-- Money Info -->
<div style="display: grid; grid-template-columns: repeat(${isPDA ? '1' : '2'}, 1fr); gap: 12px; margin-top: 12px;">
<div class="api-bar">
<div class="api-bar-label">Cash on Hand</div>
<div class="api-bar-value" id="money-hand">$0</div>
</div>
<div class="api-bar" id="vault-container" style="display: none;">
<div class="api-bar-label">Vault</div>
<div class="api-bar-value" id="money-vault">$0</div>
</div>
</div>
<!-- Company Info -->
<div id="company-info" style="display: none; margin-top: 12px;">
<div class="api-bar">
<div class="api-bar-label">Company: <span id="company-name">-</span></div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-top: 8px; font-size: ${isPDA ? '11px' : '12px'};">
<div>Position: <span id="company-position" style="font-weight: 600;">-</span></div>
<div>Employees: <span id="company-employees" style="font-weight: 600;">-</span></div>
</div>
</div>
</div>
</div>
<button class="timer-btn" id="refresh-api-btn" style="width: 100%; margin-top: 12px; display: none; padding: 10px;">Refresh Data</button>
</div>
<div class="mini-app-section">
<h3>📝 Quick Notes</h3>
<textarea id="quick-notes" placeholder="Jot down quick notes..." style="width: 100%; min-height: 120px; padding: 12px; border-radius: 8px; border: none; background: ${currentTheme.inputBg}; color: ${currentTheme.textColor}; font-family: monospace; resize: vertical;"></textarea>
<button class="timer-btn" id="save-notes-btn" style="margin-top: 8px; width: 100%; padding: 10px;">Save Notes</button>
</div>
<div class="mini-app-section">
<h3>⚔️ Faction Shortcuts</h3>
<div style="display: grid; grid-template-columns: repeat(${isPDA ? '1' : '2'}, 1fr); gap: 8px;">
<a href="/factions.php?step=your" class="timer-btn" style="text-decoration: none; text-align: center; display: block; padding: 10px;">Faction Home</a>
<a href="/factions.php?step=your#/tab=crimes" class="timer-btn" style="text-decoration: none; text-align: center; display: block; padding: 10px;">OC</a>
<a href="/factions.php?step=your#/tab=armoury" class="timer-btn" style="text-decoration: none; text-align: center; display: block; padding: 10px;">Armory</a>
<a href="/factions.php?step=your#/tab=controls" class="timer-btn" style="text-decoration: none; text-align: center; display: block; padding: 10px;">Controls</a>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(miniAppsOverlay);
function openMiniApps() {
if (isOpen) toggleMenu();
miniAppsOverlay.classList.add('show');
renderTimers();
loadNotes();
updateAPIMonitor();
}
function renderTimers() {
const timersList = document.getElementById('timers-list');
if (!timers || timers.length === 0) {
timersList.innerHTML = '<div style="text-align: center; padding: 20px; color: ' + currentTheme.textSecondary + ';">No active timers</div>';
return;
}
timersList.innerHTML = timers.map((timer, i) => {
const remaining = Math.max(0, timer.endTime - Date.now());
const minutes = Math.floor(remaining / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
return `
<div class="timer-item">
<div>
<div style="font-weight: 600;">${timer.name}</div>
<div style="font-size: 12px; color: ${currentTheme.textSecondary};">${minutes}m ${seconds}s remaining</div>
</div>
<button class="timer-btn danger" onclick="window.tornRadialRemoveTimer(${i})">Remove</button>
</div>
`;
}).join('');
}
window.tornRadialRemoveTimer = function(index) {
timers.splice(index, 1);
Storage.set('tornRadialTimers', timers);
renderTimers();
};
document.getElementById('add-timer-btn').addEventListener('click', () => {
const name = document.getElementById('timer-name').value.trim();
const minutes = parseInt(document.getElementById('timer-minutes').value);
if (!name || !minutes || minutes <= 0) {
showNotification('⚠️ Please enter timer name and duration');
return;
}
const timer = {
name: name,
endTime: Date.now() + (minutes * 60000)
};
timers.push(timer);
Storage.set('tornRadialTimers', timers);
document.getElementById('timer-name').value = '';
document.getElementById('timer-minutes').value = '';
renderTimers();
showNotification(`✅ Timer "${name}" added for ${minutes} minutes`);
setTimeout(() => {
if (settings.notifications.enabled) {
showNotification(`⏰ Timer Complete: ${name}`);
if (typeof GM_notification !== 'undefined') {
GM_notification({
title: 'Torn Radial Timer',
text: `Timer "${name}" has finished!`,
timeout: 5000
});
}
}
}, minutes * 60000);
});
// API Monitor
async function updateAPIMonitor() {
if (!settings.apiKey) {
document.getElementById('api-data-container').style.display = 'none';
document.getElementById('refresh-api-btn').style.display = 'none';
return;
}
try {
// Fetch bars, money, and basic profile
const [barsData, moneyData, profileData] = await Promise.all([
API.request('/user/?selections=bars'),
API.request('/user/?selections=money'),
API.request('/user/?selections=profile,job')
]);
document.getElementById('api-data-container').style.display = 'block';
document.getElementById('refresh-api-btn').style.display = 'block';
document.getElementById('api-status').style.display = 'none';
// Helper function to format time
const formatTime = (seconds) => {
if (seconds <= 0) return 'Full';
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
};
// Update Energy
const energyCurrent = barsData.energy.current;
const energyMax = barsData.energy.maximum;
const energyPercent = (energyCurrent / energyMax) * 100;
const energyFulltime = barsData.energy.fulltime || 0;
document.getElementById('energy-value').textContent = `${energyCurrent}/${energyMax}`;
document.getElementById('energy-progress').style.width = energyPercent + '%';
document.getElementById('energy-time').textContent = energyFulltime > 0 ? `Full in ${formatTime(energyFulltime)}` : 'Full';
// Update Nerve
const nerveCurrent = barsData.nerve.current;
const nerveMax = barsData.nerve.maximum;
const nervePercent = (nerveCurrent / nerveMax) * 100;
const nerveFulltime = barsData.nerve.fulltime || 0;
document.getElementById('nerve-value').textContent = `${nerveCurrent}/${nerveMax}`;
document.getElementById('nerve-progress').style.width = nervePercent + '%';
document.getElementById('nerve-time').textContent = nerveFulltime > 0 ? `Full in ${formatTime(nerveFulltime)}` : 'Full';
// Update Happy
const happyCurrent = barsData.happy.current;
const happyMax = barsData.happy.maximum;
const happyPercent = (happyCurrent / happyMax) * 100;
const happyFulltime = barsData.happy.fulltime || 0;
document.getElementById('happy-value').textContent = `${happyCurrent}/${happyMax}`;
document.getElementById('happy-progress').style.width = happyPercent + '%';
document.getElementById('happy-time').textContent = happyFulltime > 0 ? `Full in ${formatTime(happyFulltime)}` : 'Full';
// Update Life
const lifeCurrent = barsData.life.current;
const lifeMax = barsData.life.maximum;
const lifePercent = (lifeCurrent / lifeMax) * 100;
document.getElementById('life-value').textContent = `${Math.floor(lifeCurrent)}/${lifeMax}`;
document.getElementById('life-progress').style.width = lifePercent + '%';
// Update Money
const formatMoney = (amount) => {
return '$' + amount.toLocaleString();
};
document.getElementById('money-hand').textContent = formatMoney(moneyData.money_onhand || 0);
// Show vault if user has one
if (moneyData.vault_amount !== undefined) {
document.getElementById('vault-container').style.display = 'block';
document.getElementById('money-vault').textContent = formatMoney(moneyData.vault_amount || 0);
}
// Update Company Info (if they own/work at one)
if (profileData.job && profileData.job.company_id) {
try {
const companyData = await API.request(`/company/?selections=profile,employees`);
document.getElementById('company-info').style.display = 'block';
document.getElementById('company-name').textContent = companyData.company.name || 'Unknown';
document.getElementById('company-position').textContent = profileData.job.position || 'Employee';
document.getElementById('company-employees').textContent = Object.keys(companyData.company_employees || {}).length || '0';
} catch(e) {
// Not a company owner, hide section
document.getElementById('company-info').style.display = 'none';
}
} else {
document.getElementById('company-info').style.display = 'none';
}
} catch(e) {
ErrorLogger.log('error', 'Failed to fetch API data', e);
document.getElementById('api-status').innerHTML = '<p style="color: ' + currentTheme.dangerColor + '; font-size: 12px;">Failed to fetch API data. Check your API key.</p>';
document.getElementById('api-status').style.display = 'block';
document.getElementById('api-data-container').style.display = 'none';
}
}
document.getElementById('refresh-api-btn').addEventListener('click', () => {
updateAPIMonitor();
showNotification('🔄 API data refreshed');
});
// Notes
function loadNotes() {
const notes = Storage.get('tornRadialNotes', '');
document.getElementById('quick-notes').value = notes;
}
document.getElementById('save-notes-btn').addEventListener('click', () => {
const notes = document.getElementById('quick-notes').value;
Storage.set('tornRadialNotes', notes);
showNotification('✅ Notes saved!');
});
document.getElementById('mini-apps-close').addEventListener('click', () => {
miniAppsOverlay.classList.remove('show');
});
miniAppsOverlay.addEventListener('click', (e) => {
if (e.target === miniAppsOverlay) {
miniAppsOverlay.classList.remove('show');
}
});
// Timer update loop
setInterval(() => {
if (miniAppsOverlay.classList.contains('show')) {
renderTimers();
}
timers = timers.filter(timer => {
if (Date.now() >= timer.endTime) {
if (settings.notifications.enabled) {
showNotification(`⏰ Timer Complete: ${timer.name}`);
if (typeof GM_notification !== 'undefined') {
GM_notification({
title: 'Torn Radial Timer',
text: `Timer "${timer.name}" has finished!`,
timeout: 5000
});
}
}
return false;
}
return true;
});
Storage.set('tornRadialTimers', timers);
}, 1000);
// ==================== SCREEN CALIBRATION ====================
const calibrationOverlay = document.createElement('div');
calibrationOverlay.id = 'torn-radial-calibration';
calibrationOverlay.innerHTML = `
<div class="calibration-instruction">Click the TOP-LEFT corner of your safe area</div>
<button class="calibration-cancel" id="calibration-cancel-btn">Cancel (ESC)</button>
`;
document.body.appendChild(calibrationOverlay);
function startCalibration() {
calibrationMode = true;
calibrationStep = 0;
calibrationOverlay.classList.add('show');
const deviceType = isPDA ? 'mobile' : 'desktop';
document.querySelector('.calibration-instruction').textContent = `Click the TOP-LEFT corner of your safe area (${deviceType} mode)`;
}
calibrationOverlay.addEventListener('click', (e) => {
if (e.target.classList.contains('calibration-cancel') || e.target.id === 'calibration-cancel-btn') {
calibrationMode = false;
calibrationOverlay.classList.remove('show');
const points = document.querySelectorAll('.calibration-point');
points.forEach(p => p.remove());
return;
}
if (calibrationMode) {
if (calibrationStep === 0) {
const point = document.createElement('div');
point.className = 'calibration-point';
point.style.left = e.clientX + 'px';
point.style.top = e.clientY + 'px';
calibrationOverlay.appendChild(point);
const tempCalibration = {
topLeft: { x: e.clientX, y: e.clientY }
};
if (isPDA) {
settings.screenCalibrationMobile = tempCalibration;
} else {
settings.screenCalibration = tempCalibration;
}
calibrationStep = 1;
const deviceType = isPDA ? 'mobile' : 'desktop';
document.querySelector('.calibration-instruction').textContent = `Click the BOTTOM-RIGHT corner of your safe area (${deviceType} mode)`;
} else if (calibrationStep === 1) {
const point = document.createElement('div');
point.className = 'calibration-point';
point.style.left = e.clientX + 'px';
point.style.top = e.clientY + 'px';
calibrationOverlay.appendChild(point);
if (isPDA) {
settings.screenCalibrationMobile.bottomRight = { x: e.clientX, y: e.clientY };
} else {
settings.screenCalibration.bottomRight = { x: e.clientX, y: e.clientY };
}
Storage.set('tornRadialSettings', settings);
calibrationMode = false;
const deviceType = isPDA ? 'Mobile' : 'Desktop';
showNotification(`✅ ${deviceType} screen calibration complete!`);
setTimeout(() => {
calibrationOverlay.classList.remove('show');
const points = document.querySelectorAll('.calibration-point');
points.forEach(p => p.remove());
createRadialItems();
}, 1000);
}
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && calibrationMode) {
calibrationMode = false;
calibrationOverlay.classList.remove('show');
const points = document.querySelectorAll('.calibration-point');
points.forEach(p => p.remove());
}
// Global keyboard shortcut
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'R') {
e.preventDefault();
openSettings();
showNotification('⚙️ Settings opened via keyboard shortcut');
}
});
// ==================== SETTINGS MODAL WITH TABS ====================
function moveLink(fromIndex, direction) {
try {
const toIndex = direction === 'up' ? fromIndex - 1 : fromIndex + 1;
if (toIndex < 0 || toIndex >= links.length) return;
const temp = links[fromIndex];
links[fromIndex] = links[toIndex];
links[toIndex] = temp;
if (!loadouts[settings.currentLoadout]) {
loadouts[settings.currentLoadout] = { name: settings.currentLoadout, links: [] };
}
loadouts[settings.currentLoadout].links = JSON.parse(JSON.stringify(links));
Storage.set('tornRadialLoadouts', loadouts);
renderLinksContainer();
} catch(e) {
ErrorLogger.log('error', 'Failed to move link', e);
}
}
const modal = document.createElement('div');
modal.id = 'torn-radial-modal';
// Generate theme options dynamically
const themeOptions = getThemeNames().map(themeName =>
`<option value="${themeName}">${THEMES[themeName].name}</option>`
).join('');
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div class="modal-header-title">
<h2>⚙️ Quick Travel Settings</h2>
</div>
<button class="error-log-btn" id="show-error-log" title="View Error Log">🐞</button>
<button class="modal-close" id="modal-close-btn">✕</button>
</div>
<div class="modal-tabs">
<button class="modal-tab active" data-tab="general">General</button>
<button class="modal-tab" data-tab="appearance">Appearance</button>
<button class="modal-tab" data-tab="advanced">Advanced</button>
<button class="modal-tab" data-tab="about">About</button>
<button class="modal-tab" data-tab="donators">Donators</button>
</div>
<div class="modal-body">
<!-- GENERAL TAB -->
<div class="tab-content active" data-tab="general">
<div class="loadout-selector">
<h3>📋 Loadouts</h3>
<div class="loadout-tabs" id="loadout-tabs"></div>
<div class="loadout-actions">
<button id="new-loadout-btn">➕ New</button>
<button id="rename-loadout-btn">✏️ Rename</button>
<button class="delete" id="delete-loadout-btn">🗑️ Delete</button>
</div>
</div>
<button class="add-current-page-btn" id="add-current-page-btn">📄 Add Current Page to Loadout</button>
<div id="links-container"></div>
</div>
<!-- APPEARANCE TAB -->
<div class="tab-content" data-tab="appearance">
<div class="settings-section">
<h3>🎨 Visual Settings</h3>
<div class="setting-item">
<label>Theme</label>
<select id="theme-select">
${themeOptions}
</select>
</div>
<div class="setting-item">
<label>Layout</label>
<select id="layout-select">
<option value="circular">Circular</option>
<option value="vertical">Vertical</option>
<option value="horizontal">Horizontal</option>
</select>
</div>
<div class="setting-item">
<label>Size</label>
<select id="size-select">
<option value="pda">PDA</option>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</div>
</div>
</div>
<!-- ADVANCED TAB -->
<div class="tab-content" data-tab="advanced">
<div class="settings-section">
<h3>🔔 Notifications</h3>
<div class="setting-item">
<label>Enable Notifications</label>
<input type="checkbox" id="notif-enabled">
</div>
</div>
<div class="settings-section">
<h3>🔑 API Configuration</h3>
<div class="setting-item" style="flex-direction: column; align-items: stretch;">
<label>Torn API Key</label>
<input type="password" id="api-key-input" placeholder="Enter your Torn API key" style="width: 100%; margin-top: 8px;">
<p style="font-size: ${isPDA ? '10px' : '11px'}; color: ${currentTheme.textSecondary}; margin: 8px 0 0 0;">Used for API Monitor in Mini Apps. Get your key at torn.com/preferences.php#tab=api</p>
</div>
</div>
<div class="settings-section">
<h3>📐 Calibration</h3>
<p style="font-size: ${isPDA ? '11px' : '12px'}; color: ${currentTheme.textSecondary}; margin: 0 0 12px 0;">
Current device: <strong>${isPDA ? 'Mobile/PDA' : 'Desktop'}</strong>
</p>
<p style="font-size: ${isPDA ? '11px' : '12px'}; color: ${currentTheme.textSecondary}; margin: 0 0 12px 0;">
Calibration ensures all menu items stay within your screen boundaries. Each device type (desktop/mobile) has separate calibration settings.
</p>
<button class="btn-calibrate" id="calibrate-btn-advanced" style="width: 100%; padding: ${isPDA ? '10px' : '12px'};">
📐 Calibrate ${isPDA ? 'Mobile' : 'Desktop'} Screen
</button>
</div>
</div>
<!-- ABOUT TAB -->
<div class="tab-content" data-tab="about">
<div class="about-section">
<h3>ℹ️ About Torn Quick-Travel</h3>
<div class="version-badge">v1.6.1</div>
<p><strong>Author:</strong> Sensimillia (2168012)</p>
<p>Radial navigation menu for Torn City with enhanced features, API integration, customization options.</p>
</div>
<div class="about-section">
<h3>✨ What's New in v1.6.1</h3>
<ul>
<li><strong>🔍 Search Icon:</strong> Search has its own radial menu icon for easy access (WIP)</li>
<li><strong>📱 PDA Mode:</strong> Fully responsive settings UI that scales perfectly on mobile devices</li>
<li><strong>🎨 Tabbed Settings:</strong> Cleaner, organized interface with categorized tabs</li>
<li><strong>📐 Dual Calibration:</strong> Separate screen calibration for desktop and mobile/PDA modes (WIP)</li>
<li><strong>🔔 Smart Notifications:</strong> Throttled notification system prevents spam and browser crashes</li>
<li><strong>⚡ Optimized Code:</strong> Dynamic theme system and better structured architecture</li>
<li><strong>🧹 Cleaner Menu:</strong> Removed redundant script manager items</li>
</ul>
</div>
<div class="about-section">
<h3>🎯 Features</h3>
<ul>
<li>Customizable radial menu with multiple layouts</li>
<li>Multiple loadout presets for different activities</li>
<li>Smart search with history and categorized results</li>
<li>Built-in calculator with keyboard support</li>
<li>Timer manager and quick notes</li>
<li>API integration for real-time stats monitoring</li>
<li>10 beautiful themes to choose from</li>
<li>Import/Export settings for backup and sharing</li>
<li>Usage statistics and automatic favorites</li>
<li>Comprehensive error logging system</li>
</ul>
</div>
<div class="about-section">
<h3>⌨️ Keyboard Shortcuts</h3>
<ul>
<li><strong>Ctrl/Cmd + Shift + R:</strong> Open settings</li>
<li><strong>↑↓ + Enter:</strong> Navigate search results</li>
<li><strong>Escape:</strong> Close overlays/cancel calibration</li>
<li><strong>Calculator:</strong> Full keyboard support for all operations</li>
</ul>
</div>
<div class="about-section">
<h3>💡 Tips</h3>
<ul>
<li>Drag the main button to reposition the menu anywhere on screen</li>
<li>Use calibration to set safe boundaries for your specific screen layout</li>
<li>Add frequently visited pages directly from any Torn page</li>
<li>The Favorites loadout auto-generates from your most-used links</li>
<li>Export your settings regularly to prevent data loss</li>
</ul>
</div>
</div>
<!-- DONATORS TAB -->
<div class="tab-content" data-tab="donators">
<div class="about-section">
<h3>💖 Thank You to Our Supporters</h3>
<p style="font-size: ${isPDA ? '12px' : '14px'}; color: ${currentTheme.textSecondary};">
These amazing people have supported the development of Torn Quick-Travel. Your contributions help keep this project alive and evolving!
</p>
</div>
<div class="about-section">
<div class="donators-list" id="donators-list">
<!-- Placeholder for future donators -->
<div class="donator-item">
<div class="donator-icon">🌟</div>
<div class="donator-info">
<div class="donator-name">Caio [2488372]</div>
<div class="donator-amount">30x Can of Crocozade</div>
</div>
</div>
</div>
</div>
<div class="about-section">
<h3>☕ Support Development</h3>
<p style="font-size: ${isPDA ? '12px' : '14px'}; color: ${currentTheme.textSecondary}; margin-bottom: 12px;">
If you enjoy using Torn Quick-Travel and would like to support its continued development, consider sending a small donation in-game or leaving a positive review!
</p>
<button class="btn-coffee" id="coffee-btn-about" style="width: 100%; padding: ${isPDA ? '12px' : '14px'};">
☕ Visit My Profile
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-success" id="add-link-btn">➕ Add Link</button>
<button class="btn-secondary" id="restore-btn">🔄 Restore</button>
<button class="btn-secondary" id="export-btn">📤 Export</button>
<button class="btn-secondary" id="import-btn">📥 Import</button>
<button class="btn-primary" id="save-btn">💾 Save Changes</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Tab switching functionality
const tabButtons = modal.querySelectorAll('.modal-tab');
const tabContents = modal.querySelectorAll('.tab-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.dataset.tab;
// Update button states
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Update content visibility
tabContents.forEach(content => {
if (content.dataset.tab === targetTab) {
content.classList.add('active');
} else {
content.classList.remove('active');
}
});
});
});
modal.addEventListener('click', (e) => {
if (e.target === modal) closeSettings();
});
document.getElementById('show-error-log').addEventListener('click', (e) => {
e.preventDefault();
showErrorLog();
});
function renderLoadoutTabs() {
try {
const tabsContainer = document.getElementById('loadout-tabs');
if (!tabsContainer) return;
tabsContainer.innerHTML = '';
Object.keys(loadouts).forEach(key => {
const tab = document.createElement('button');
tab.className = 'loadout-tab';
tab.textContent = loadouts[key]?.name || key;
tab.dataset.loadout = key;
if (key === settings.currentLoadout) {
tab.classList.add('active');
}
tab.addEventListener('click', () => switchLoadout(key));
tabsContainer.appendChild(tab);
});
} catch(e) {
ErrorLogger.log('error', 'Failed to render loadout tabs', e);
}
}
function switchLoadout(loadoutKey) {
try {
if (!loadouts[loadoutKey]) {
ErrorLogger.log('error', `Loadout not found: ${loadoutKey}`);
return;
}
settings.currentLoadout = loadoutKey;
links = loadouts[loadoutKey].links.map(link => ({
name: link?.name || 'Unnamed',
url: link?.url || '/index.php',
icon: link?.icon || '🔗',
color: link?.color || '#4aa3df'
}));
renderLoadoutTabs();
renderLinksContainer();
} catch(e) {
ErrorLogger.log('error', 'Failed to switch loadout', e);
}
}
function renderLinksContainer() {
try {
const linksContainer = document.getElementById('links-container');
if (!linksContainer) return;
linksContainer.innerHTML = '';
links.forEach((link, i) => {
const linkItem = document.createElement('div');
linkItem.className = 'link-item';
linkItem.innerHTML = `
<input type="text" value="${link.icon}" placeholder="Icon" maxlength="2" data-index="${i}" data-field="icon">
<input type="text" value="${link.name}" placeholder="Name" data-index="${i}" data-field="name">
<input type="text" value="${link.url}" placeholder="URL" data-index="${i}" data-field="url">
<div class="color-picker-wrapper">
<input type="color" class="color-picker" value="${link.color}" data-index="${i}" data-field="color">
</div>
<button class="delete-btn" data-index="${i}" title="Delete">🗑️</button>
<div class="reorder-controls">
<button class="reorder-btn" data-index="${i}" data-direction="up" ${i === 0 ? 'disabled' : ''} title="Move Up">▲</button>
<button class="reorder-btn" data-index="${i}" data-direction="down" ${i === links.length - 1 ? 'disabled' : ''} title="Move Down">▼</button>
</div>
`;
linksContainer.appendChild(linkItem);
});
linksContainer.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const index = parseInt(this.dataset.index);
if (confirm(`Remove "${links[index].name}"?`)) {
try {
links.splice(index, 1);
saveCurrentLoadout();
renderLinksContainer();
} catch(err) {
ErrorLogger.log('error', 'Failed to delete link', err);
showNotification('❌ Failed to delete link');
}
}
});
});
linksContainer.querySelectorAll('.reorder-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (!this.disabled) {
const index = parseInt(this.dataset.index);
const direction = this.dataset.direction;
moveLink(index, direction);
}
});
});
} catch(e) {
ErrorLogger.log('error', 'Failed to render links container', e);
}
}
function openSettings() {
try {
if (isOpen) toggleMenu();
document.getElementById('theme-select').value = settings.theme;
document.getElementById('layout-select').value = settings.layout;
document.getElementById('size-select').value = settings.iconSize;
document.getElementById('notif-enabled').checked = settings.notifications.enabled;
document.getElementById('api-key-input').value = settings.apiKey || '';
renderLoadoutTabs();
renderLinksContainer();
modal.classList.add('show');
} catch(e) {
ErrorLogger.log('error', 'Failed to open settings', e);
}
}
function saveCurrentLoadout() {
try {
document.querySelectorAll('.link-item input').forEach(input => {
const index = parseInt(input.dataset.index);
const field = input.dataset.field;
if (links[index] && field) {
links[index][field] = input.value;
}
});
if (!loadouts[settings.currentLoadout]) {
loadouts[settings.currentLoadout] = { name: settings.currentLoadout, links: [] };
}
loadouts[settings.currentLoadout].links = JSON.parse(JSON.stringify(links));
Storage.set('tornRadialLoadouts', loadouts);
} catch(e) {
ErrorLogger.log('error', 'Failed to save loadout', e);
}
}
function closeSettings() {
try {
const oldLayout = settings.layout;
const oldSize = settings.iconSize;
const oldTheme = settings.theme;
settings.theme = document.getElementById('theme-select').value;
settings.layout = document.getElementById('layout-select').value;
settings.iconSize = document.getElementById('size-select').value;
settings.notifications.enabled = document.getElementById('notif-enabled').checked;
const newApiKey = document.getElementById('api-key-input').value.trim();
if (newApiKey !== settings.apiKey) {
settings.apiKey = newApiKey;
API.setApiKey(newApiKey);
}
Storage.set('tornRadialSettings', settings);
saveCurrentLoadout();
modal.classList.remove('show');
if (oldLayout !== settings.layout || oldSize !== settings.iconSize || oldTheme !== settings.theme) {
location.reload();
} else {
createRadialItems();
}
} catch(e) {
ErrorLogger.log('error', 'Failed to close settings', e);
}
}
// Event listeners
document.getElementById('modal-close-btn').addEventListener('click', (e) => {
e.preventDefault();
closeSettings();
});
document.getElementById('save-btn').addEventListener('click', (e) => {
e.preventDefault();
closeSettings();
});
document.getElementById('add-link-btn').addEventListener('click', (e) => {
e.preventDefault();
try {
saveCurrentLoadout();
links.push({ name: 'New Link', url: '/index.php', icon: '🔗', color: '#4aa3df' });
saveCurrentLoadout();
renderLinksContainer();
// Switch to General tab when adding link
document.querySelector('.modal-tab[data-tab="general"]').click();
} catch(e) {
ErrorLogger.log('error', 'Failed to add link', e);
}
});
document.getElementById('add-current-page-btn').addEventListener('click', (e) => {
e.preventDefault();
addCurrentPage();
});
document.getElementById('restore-btn').addEventListener('click', (e) => {
e.preventDefault();
if (confirm('Restore default links for this loadout?')) {
try {
links = JSON.parse(JSON.stringify(DEFAULT_LINKS));
saveCurrentLoadout();
renderLinksContainer();
showNotification('✅ Loadout restored to defaults');
} catch(e) {
ErrorLogger.log('error', 'Failed to restore defaults', e);
}
}
});
document.getElementById('export-btn').addEventListener('click', (e) => {
e.preventDefault();
Storage.exportAll();
showNotification('✅ Data exported successfully');
});
document.getElementById('import-btn').addEventListener('click', (e) => {
e.preventDefault();
importLoadouts();
});
document.getElementById('calibrate-btn-advanced').addEventListener('click', (e) => {
e.preventDefault();
modal.classList.remove('show');
setTimeout(() => {
startCalibration();
}, 300);
});
document.getElementById('new-loadout-btn').addEventListener('click', (e) => {
e.preventDefault();
try {
const name = prompt('Enter loadout name:');
if (name && name.trim()) {
const key = name.toLowerCase().replace(/[^a-z0-9]/g, '_');
if (loadouts[key]) {
showNotification('⚠️ A loadout with this name already exists!');
return;
}
loadouts[key] = { name: name.trim(), links: [...DEFAULT_LINKS] };
Storage.set('tornRadialLoadouts', loadouts);
switchLoadout(key);
showNotification(`✅ Loadout "${name}" created`);
}
} catch(e) {
ErrorLogger.log('error', 'Failed to create loadout', e);
}
});
document.getElementById('rename-loadout-btn').addEventListener('click', (e) => {
e.preventDefault();
try {
if (settings.currentLoadout === 'default') {
showNotification('⚠️ Cannot rename the default loadout!');
return;
}
const newName = prompt('Enter new name:', loadouts[settings.currentLoadout].name);
if (newName && newName.trim()) {
loadouts[settings.currentLoadout].name = newName.trim();
Storage.set('tornRadialLoadouts', loadouts);
renderLoadoutTabs();
showNotification(`✅ Loadout renamed to "${newName}"`);
}
} catch(e) {
ErrorLogger.log('error', 'Failed to rename loadout', e);
}
});
document.getElementById('delete-loadout-btn').addEventListener('click', (e) => {
e.preventDefault();
try {
if (settings.currentLoadout === 'default') {
showNotification('⚠️ Cannot delete the default loadout!');
return;
}
if (confirm(`Delete "${loadouts[settings.currentLoadout].name}" loadout?`)) {
const deletedName = loadouts[settings.currentLoadout].name;
delete loadouts[settings.currentLoadout];
Storage.set('tornRadialLoadouts', loadouts);
switchLoadout('default');
showNotification(`✅ Loadout "${deletedName}" deleted`);
}
} catch(e) {
ErrorLogger.log('error', 'Failed to delete loadout', e);
}
});
document.getElementById('coffee-btn-about').addEventListener('click', (e) => {
e.preventDefault();
window.open('https://www.torn.com/profiles.php?XID=2168012#/', '_blank');
});
// ==================== HELPER FUNCTIONS FOR SETTINGS ====================
function addCurrentPage() {
const currentUrl = window.location.pathname + window.location.search;
const pageName = prompt('Enter a name for this page:', document.title.split(' - ')[0] || 'Current Page');
if (pageName && pageName.trim()) {
const newLink = {
name: pageName.trim(),
url: currentUrl,
icon: '📄',
color: currentTheme.primaryColor
};
links.push(newLink);
saveCurrentLoadout();
renderLinksContainer();
showNotification(`✅ Added "${pageName}" to current loadout`);
}
}
function importLoadouts() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
if (Storage.importAll(e.target.result)) {
showNotification('✅ Data imported successfully!');
setTimeout(() => location.reload(), 1500);
} else {
showNotification('❌ Import failed - invalid file');
}
} catch(err) {
ErrorLogger.log('error', 'Import failed', err);
showNotification('❌ Import failed - invalid file');
}
};
reader.readAsText(file);
}
};
input.click();
}
// ==================== ERROR LOG MODAL ====================
const errorLogModal = document.createElement('div');
errorLogModal.id = 'error-log-modal';
errorLogModal.innerHTML = `
<div class="error-log-content" style="background: ${currentTheme.modalBg}; border-radius: ${isPDA ? '16px' : '20px'}; padding: 0; max-width: ${isPDA ? '95%' : '800px'}; width: 100%; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 32px 64px rgba(0, 0, 0, 0.4);">
<div class="error-log-header" style="padding: ${isPDA ? '16px 20px' : '20px 24px'}; border-bottom: 1px solid ${currentTheme.borderColor}; background: ${currentTheme.modalHeaderBg}; display: flex; justify-content: space-between; align-items: center;">
<h2 style="margin: 0; font-size: ${isPDA ? '16px' : '20px'}; color: ${currentTheme.textColor};">🐞 Error Log</h2>
<button class="modal-close" id="error-log-close">✕</button>
</div>
<div class="error-log-body" id="error-log-body" style="padding: ${isPDA ? '16px' : '20px'}; overflow-y: auto; flex: 1; font-family: 'Courier New', monospace; font-size: ${isPDA ? '11px' : '12px'}; color: ${currentTheme.textColor};"></div>
<div class="error-log-footer" style="padding: ${isPDA ? '12px 16px' : '16px 24px'}; border-top: 1px solid ${currentTheme.borderColor}; background: ${currentTheme.modalFooterBg}; display: flex; gap: ${isPDA ? '8px' : '12px'};">
<button class="btn-secondary" id="export-log-btn" style="flex: 1; padding: ${isPDA ? '10px' : '12px'}; border: none; border-radius: ${isPDA ? '8px' : '10px'}; font-weight: 600; cursor: pointer;">💾 Export</button>
<button class="btn-secondary" id="clear-log-btn" style="flex: 1; padding: ${isPDA ? '10px' : '12px'}; border: none; border-radius: ${isPDA ? '8px' : '10px'}; font-weight: 600; cursor: pointer;">🗑️ Clear</button>
<button class="btn-primary" id="close-log-btn" style="flex: 1; padding: ${isPDA ? '10px' : '12px'}; border: none; border-radius: ${isPDA ? '8px' : '10px'}; font-weight: 600; cursor: pointer;">Close</button>
</div>
</div>
`;
errorLogModal.style.cssText = `display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000001; justify-content: center; align-items: center; padding: ${isPDA ? '10px' : '20px'};`;
document.body.appendChild(errorLogModal);
function showErrorLog() {
try {
const logBody = document.getElementById('error-log-body');
const logs = ErrorLogger.getLogs();
if (logs.length === 0) {
logBody.innerHTML = '<div style="text-align: center; padding: 40px; opacity: 0.5;">No errors logged</div>';
} else {
logBody.innerHTML = logs.reverse().map(log => `
<div style="background: ${currentTheme.sectionBg}; padding: ${isPDA ? '10px' : '12px'}; margin-bottom: ${isPDA ? '10px' : '12px'}; border-radius: ${isPDA ? '8px' : '10px'}; border-left: 4px solid ${log.type === 'error' ? '#FF3B30' : log.type === 'warning' ? '#FF9500' : '#007AFF'};">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px; font-weight: bold; font-size: ${isPDA ? '10px' : '11px'};">
<span style="text-transform: uppercase;">${log.type}</span>
<span style="opacity: 0.7; font-size: ${isPDA ? '9px' : '10px'};">${new Date(log.timestamp).toLocaleString()}</span>
</div>
<div style="margin-bottom: 8px;">${log.message}</div>
${log.error ? `<div style="font-size: ${isPDA ? '9px' : '10px'}; opacity: 0.6; white-space: pre-wrap; word-break: break-all;">${log.error.message}\n${log.error.stack || ''}</div>` : ''}
</div>
`).join('');
}
errorLogModal.style.display = 'flex';
} catch(e) {
ErrorLogger.log('error', 'Failed to show error log', e);
alert('Error displaying log. Check console.');
}
}
document.getElementById('error-log-close').addEventListener('click', () => {
errorLogModal.style.display = 'none';
});
document.getElementById('close-log-btn').addEventListener('click', () => {
errorLogModal.style.display = 'none';
});
document.getElementById('export-log-btn').addEventListener('click', () => {
try {
ErrorLogger.exportLogs();
showNotification('✅ Error logs exported');
} catch(e) {
ErrorLogger.log('error', 'Failed to export logs', e);
showNotification('❌ Failed to export logs');
}
});
document.getElementById('clear-log-btn').addEventListener('click', () => {
if (confirm('Clear all error logs?')) {
ErrorLogger.clear();
showErrorLog();
showNotification('✅ Error logs cleared');
}
});
errorLogModal.addEventListener('click', (e) => {
if (e.target === errorLogModal) {
errorLogModal.style.display = 'none';
}
});
// ==================== RESET FUNCTIONS ====================
function resetPosition() {
try {
const safePos = getSafeInitialPosition();
savedPos.x = safePos.x;
savedPos.y = safePos.y;
container.style.left = safePos.x + 'px';
container.style.top = safePos.y + 'px';
Storage.set(positionKey, savedPos);
createRadialItems();
if (isOpen) {
isOpen = false;
setTimeout(() => toggleMenu(), 100);
}
showNotification('✅ Menu position reset to center');
return true;
} catch(e) {
ErrorLogger.log('error', 'Failed to reset position', e);
return false;
}
}
function fullReset() {
try {
if (confirm('⚠️ FULL RESET will:\n- Delete ALL custom loadouts\n- Reset ALL settings to defaults\n- Clear error logs\n- Clear usage statistics\n\nThis CANNOT be undone!\n\nAre you absolutely sure?')) {
if (confirm('Last chance! This will delete everything. Continue?')) {
localStorage.removeItem('tornRadialSettings');
localStorage.removeItem('tornRadialLoadouts');
localStorage.removeItem('tornRadialPosition');
localStorage.removeItem('tornRadialPositionMobile');
localStorage.removeItem('tornRadialErrorLogs');
localStorage.removeItem('tornRadialUsageStats');
localStorage.removeItem('tornRadialTimers');
localStorage.removeItem('tornRadialSearchHistory');
localStorage.removeItem('tornRadialNotes');
localStorage.removeItem('tornRadialApiKey');
localStorage.removeItem('tornRadialCalibration');
localStorage.removeItem('tornRadialCalibrationMobile');
alert('✅ Full reset complete! Refreshing page...');
location.reload();
return true;
}
}
return false;
} catch(e) {
alert('Reset failed. Try clearing localStorage manually.');
console.error('Reset error:', e);
return false;
}
}
// ==================== MENU COMMANDS (CLEANED UP) ====================
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand('⚙️ Open Settings', () => {
openSettings();
});
GM_registerMenuCommand('📐 Calibrate Screen', () => {
startCalibration();
});
GM_registerMenuCommand('📤 Export All Data', () => {
Storage.exportAll();
showNotification('✅ Data exported successfully');
});
GM_registerMenuCommand('📥 Import Data', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
if (Storage.importAll(e.target.result)) {
alert('Import successful! Page will reload.');
location.reload();
} else {
alert('Import failed. Check file format.');
}
};
reader.readAsText(file);
}
};
input.click();
});
GM_registerMenuCommand('🎯 Reset Position', () => {
if (resetPosition()) {
alert('Menu position has been reset to center!');
}
});
GM_registerMenuCommand('⚠️ Full Reset', () => {
fullReset();
});
GM_registerMenuCommand('🐞 View Error Log', () => {
showErrorLog();
});
}
// ==================== CONSOLE API ====================
window.tornRadialReset = fullReset;
window.tornRadialResetPosition = resetPosition;
window.tornRadialSettings = openSettings;
window.tornRadialSearch = openSearch;
window.tornRadialCalculator = openCalculator;
window.tornRadialMiniApps = openMiniApps;
window.tornRadialCalibrate = startCalibration;
window.tornRadialExport = () => Storage.exportAll();
window.tornRadialAddCurrentPage = addCurrentPage;
// ==================== STARTUP LOG ====================
console.log('%c🎯 Torn Quick-Travel v1.6.1', 'font-size: 16px; font-weight: bold; color: #4aa3df;');
console.log('%c✨ NEW in v1.6.1:', 'font-size: 14px; font-weight: bold; color: #3ea34a;');
console.log('%c 🔍 Search Icon - Now visible in radial menu!', 'font-size: 12px; color: #4aa3df;');
console.log('%c 📱 PDA Mode - Fully responsive settings UI', 'font-size: 12px; color: #4aa3df;');
console.log('%c 🎨 Tabbed Settings - Organized interface', 'font-size: 12px; color: #4aa3df;');
console.log('%c 📐 Dual Calibration - Separate desktop/mobile settings', 'font-size: 12px; color: #4aa3df;');
console.log('%c 🔔 Smart Notifications - Throttled to prevent spam', 'font-size: 12px; color: #4aa3df;');
console.log('%c ⚡ Optimized Code - Dynamic themes & better structure', 'font-size: 12px; color: #4aa3df;');
console.log('%c⚙️ Keyboard: Ctrl/Cmd + Shift + R', 'font-size: 12px; color: #3ea34a;');
console.log('%c🎯 Console API:', 'font-size: 12px; color: #FF9500;');
console.log('%c tornRadialSettings() - Open settings', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialSearch() - Open search', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialCalculator() - Open calculator', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialMiniApps() - Open mini apps', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialCalibrate() - Start calibration', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialAddCurrentPage() - Add current page', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialExport() - Export all data', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialResetPosition() - Reset position', 'font-size: 11px; color: #9b9b9b;');
console.log('%c tornRadialReset() - Full reset (delete all)', 'font-size: 11px; color: #a33a3a;');
if (typeof GM_registerMenuCommand !== 'undefined') {
console.log('%c📱 Menu: Click your userscript extension icon', 'font-size: 12px; color: #AF52DE;');
}
console.log('%c📱 Device Mode: ' + (isPDA ? 'Mobile/PDA' : 'Desktop'), 'font-size: 12px; font-weight: bold; color: #FF9500;');
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
NotificationManager.destroy();
});
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址