// ==UserScript==
// @name Last Action Viewer by Mr_Awaken[3255504]
// @namespace http://tampermonkey.net/
// @version 1
// @description Shows player's last action on long-click, with API key management
// @author Mr_Awaken
// @match https://www.torn.com/*
// @grant GM.xmlHttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.listValues
// @connect api.torn.com
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
if (typeof GM_setValue === 'undefined' && typeof GM !== 'undefined') {
const GM_getValue = function(key, defaultValue) {
let value;
try {
value = localStorage.getItem('GMcompat_' + key);
if (value !== null) {
return JSON.parse(value);
}
GM.getValue(key, defaultValue).then(val => {
if (val !== undefined) {
localStorage.setItem('GMcompat_' + key, JSON.stringify(val));
}
});
return defaultValue;
} catch (e) {
console.error('Error in GM_getValue compatibility:', e);
return defaultValue;
}
};
const GM_setValue = function(key, value) {
try {
localStorage.setItem('GMcompat_' + key, JSON.stringify(value));
GM.setValue(key, value);
} catch (e) {
console.error('Error in GM_setValue compatibility:', e);
}
};
const GM_deleteValue = function(key) {
try {
localStorage.removeItem('GMcompat_' + key);
GM.deleteValue(key);
} catch (e) {
console.error('Error in GM_deleteValue compatibility:', e);
}
};
const GM_listValues = function() {
const keys = [];
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('GMcompat_')) {
keys.push(key.substring(9));
}
}
} catch (e) {
console.error('Error in GM_listValues compatibility:', e);
}
return keys;
};
window.GM_getValue = GM_getValue;
window.GM_setValue = GM_setValue;
window.GM_deleteValue = GM_deleteValue;
window.GM_listValues = GM_listValues;
}
const CACHE_DURATION_MS = 60000;
let currentDarkMode = document.body.classList.contains('dark-mode');
const scriptSettings = {
apiKey: GM_getValue("lastActionApiKey") || "",
};
const updateStyles = () => {
let styleEl = document.getElementById('last-action-styles');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'last-action-styles';
document.head.appendChild(styleEl);
}
styleEl.textContent = `
.last-action-menu {
position: fixed;
background: ${currentDarkMode ? '#333' : '#fff'};
color: ${currentDarkMode ? '#fff' : '#333'};
border: 1px solid ${currentDarkMode ? '#555' : '#ddd'};
padding: 10px;
border-radius: 5px;
box-shadow: 0 3px 10px rgba(0,0,0,0.3);
z-index: 99999;
min-width: 200px;
max-width: 280px;
pointer-events: none;
transition: opacity 0.2s ease;
font-size: 13px;
line-height: 1.4;
}
.bazaar-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 99999;
}
.bazaar-settings-modal {
background-color: #fff;
border-radius: 8px;
padding: 24px;
width: 500px;
max-width: 95vw;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
position: relative;
z-index: 100000;
font-family: 'Arial', sans-serif;
}
.bazaar-settings-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.bazaar-settings-card {
background: #f8f9fa;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
.bazaar-settings-card h3 {
margin: 0 0 12px 0;
font-size: 16px;
color: #333;
}
.bazaar-input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.bazaar-modern-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 100%;
}
.bazaar-help-text {
font-size: 12px;
color: #666;
margin: 8px 0 0 0;
line-height: 1.4;
}
.bazaar-settings-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.bazaar-settings-save, .bazaar-settings-cancel {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.bazaar-settings-save {
background: #28a745;
color: white;
}
.bazaar-settings-cancel {
background: #6c757d;
color: white;
}
.dark-mode .bazaar-settings-modal {
background-color: #2a2a2a;
color: #e0e0e0;
border: 1px solid #444;
}
.dark-mode .bazaar-settings-title {
color: #e0e0e0;
}
.dark-mode .bazaar-settings-card {
background: #3a3a3a;
border: 1px solid #555;
}
.dark-mode .bazaar-settings-card h3 {
color: #e0e0e0;
}
.dark-mode .bazaar-modern-input {
background: #4a4a4a;
border-color: #666;
color: #e0e0e0;
}
.dark-mode .bazaar-help-text {
color: #aaa;
}
.last-action-info {
color: inherit;
font-size: inherit;
}
/* Add more styles as needed from the bazaar script */
`;
};
updateStyles();
const darkModeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
const newDarkMode = document.body.classList.contains('dark-mode');
if (newDarkMode !== currentDarkMode) {
currentDarkMode = newDarkMode;
updateStyles();
}
}
});
});
darkModeObserver.observe(document.body, { attributes: true });
function fetchJSON(url, callback) {
let retryCount = 0;
const MAX_RETRIES = 2;
const TIMEOUT_MS = 10000;
const RETRY_DELAY_MS = 2000;
function makeRequest(options) {
if (typeof GM_xmlhttpRequest !== 'undefined') {
return GM_xmlhttpRequest(options);
} else if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') {
return GM.xmlHttpRequest(options);
} else {
console.error('Neither GM_xmlhttpRequest nor GM.xmlHttpRequest are available');
options.onerror && options.onerror(new Error('XMLHttpRequest API not available'));
return null;
}
}
function attemptFetch() {
let timeoutId = setTimeout(() => {
console.warn(`Request to ${url} timed out, ${retryCount < MAX_RETRIES ? 'retrying...' : 'giving up.'}`);
if (retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(attemptFetch, RETRY_DELAY_MS);
} else {
callback(null);
}
}, TIMEOUT_MS);
makeRequest({
method: 'GET',
url,
timeout: TIMEOUT_MS,
onload: res => {
clearTimeout(timeoutId);
try {
if (res.status >= 200 && res.status < 300) {
callback(JSON.parse(res.responseText));
} else {
console.warn(`Request to ${url} failed with status ${res.status}`);
if (retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(attemptFetch, RETRY_DELAY_MS);
} else {
callback(null);
}
}
} catch (e) {
console.error(`Error parsing response from ${url}:`, e);
callback(null);
}
},
onerror: (error) => {
clearTimeout(timeoutId);
console.warn(`Request to ${url} failed:`, error);
if (retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(attemptFetch, RETRY_DELAY_MS);
} else {
callback(null);
}
},
ontimeout: () => {
clearTimeout(timeoutId);
console.warn(`Request to ${url} timed out natively`);
if (retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(attemptFetch, RETRY_DELAY_MS);
} else {
callback(null);
}
}
});
}
attemptFetch();
}
function getCache(key) {
try {
const cached = GM_getValue(key);
if (cached) {
const payload = JSON.parse(cached);
if (Date.now() - payload.timestamp < CACHE_DURATION_MS) return payload.data;
}
} catch (e) {}
return null;
}
function setCache(key, data) {
try {
GM_setValue(key, JSON.stringify({ timestamp: Date.now(), data }));
} catch (e) {}
}
function loadSettings() {
try {
scriptSettings.apiKey = GM_getValue("lastActionApiKey") || "";
} catch (e) {
console.error("Oops, settings failed to load:", e);
}
}
function saveSettings() {
try {
GM_setValue("lastActionApiKey", scriptSettings.apiKey || "");
} catch (e) {
console.error("Settings save hiccup:", e);
}
}
loadSettings();
if (!scriptSettings.apiKey) {
openLastActionSettingsModal();
}
function openLastActionSettingsModal() {
const overlay = document.createElement("div");
overlay.className = "bazaar-modal-overlay";
const modal = document.createElement("div");
modal.className = "bazaar-settings-modal";
modal.innerHTML = `
<div class="bazaar-settings-title">🛠️ Last Action Viewer Configuration</div>
<div class="bazaar-tab-content active" id="tab-settings" style="max-height: 350px; overflow-y: auto;">
<div class="bazaar-settings-grid">
<div class="bazaar-settings-card">
<h3>🔑 API Authentication</h3>
<div class="bazaar-input-group">
<input type="text" id="last-action-api-key" value="${scriptSettings.apiKey || ''}" placeholder="Enter your Torn API key" class="bazaar-modern-input" style="flex-grow: 1;">
</div>
<p class="bazaar-help-text">Enter your Torn API key to enable the last action viewer. Data remains private.</p>
</div>
</div>
</div>
<div class="bazaar-settings-buttons">
<button class="bazaar-settings-save">Save Configuration</button>
<button class="bazaar-settings-cancel">Cancel</button>
</div>
`;
overlay.appendChild(modal);
modal.querySelector('.bazaar-settings-save').addEventListener('click', () => {
scriptSettings.apiKey = modal.querySelector('#last-action-api-key').value.trim();
saveSettings();
overlay.remove();
});
modal.querySelector('.bazaar-settings-cancel').addEventListener('click', () => {
overlay.remove();
});
overlay.addEventListener('click', e => {
if (e.target === overlay) overlay.remove();
});
document.body.appendChild(overlay);
}
function addLastActionSettingsMenuItem() {
const menu = document.querySelector('.settings-menu');
if (!menu || document.querySelector('.last-action-settings-button')) return;
const li = document.createElement('li');
li.className = 'link last-action-settings-button';
const a = document.createElement('a');
a.href = '#';
const iconDiv = document.createElement('div');
iconDiv.className = 'icon-wrapper';
const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgIcon.setAttribute('class', 'default');
svgIcon.setAttribute('fill', '#fff');
svgIcon.setAttribute('stroke', 'transparent');
svgIcon.setAttribute('stroke-width', '0');
svgIcon.setAttribute('width', '16');
svgIcon.setAttribute('height', '16');
svgIcon.setAttribute('viewBox', '0 0 640 512');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M320 32c-88.4 0-160 71.6-160 160s71.6 160 160 160 160-71.6 160-160S408.4 32 320 32zm0 288c-70.7 0-128-57.3-128-128s57.3-128 128-128 128 57.3 128 128-57.3 128-128 128z');
const span = document.createElement('span');
span.textContent = 'Last Action Viewer Settings';
svgIcon.appendChild(path);
iconDiv.appendChild(svgIcon);
a.appendChild(iconDiv);
a.appendChild(span);
li.appendChild(a);
a.addEventListener('click', e => {
e.preventDefault();
document.body.click();
openLastActionSettingsModal();
});
const bazaarSettings = menu.querySelector('.bazaar-settings-button');
if (bazaarSettings) {
menu.insertBefore(li, bazaarSettings);
} else {
const logoutButton = menu.querySelector('li.logout');
if (logoutButton) {
menu.insertBefore(li, logoutButton);
} else {
menu.appendChild(li);
}
}
}
function initLastActionViewer() {
const LONG_PRESS_DURATION = 500; // 500ms for long press
let pressTimer;
let isLongPress = false;
function attachLongPressListeners(link) {
// Mouse events for desktop
link.addEventListener('mousedown', (e) => {
isLongPress = false;
pressTimer = setTimeout(() => {
isLongPress = true;
handleLongPress(e, link);
}, LONG_PRESS_DURATION);
});
link.addEventListener('mouseup', () => {
clearTimeout(pressTimer);
});
link.addEventListener('mouseleave', () => {
clearTimeout(pressTimer);
});
// Touch events for mobile
link.addEventListener('touchstart', (e) => {
isLongPress = false;
pressTimer = setTimeout(() => {
isLongPress = true;
handleLongPress(e, link);
}, LONG_PRESS_DURATION);
});
link.addEventListener('touchend', () => {
clearTimeout(pressTimer);
});
link.addEventListener('touchcancel', () => {
clearTimeout(pressTimer);
});
// Prevent normal click behavior if it was a long press
link.addEventListener('click', (e) => {
if (isLongPress) {
e.preventDefault();
e.stopPropagation();
isLongPress = false;
}
});
}
const playerLinks = document.querySelectorAll('a[href*="profiles.php?XID="]');
playerLinks.forEach(attachLongPressListeners);
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const newLinks = node.querySelectorAll('a[href*="profiles.php?XID="]');
newLinks.forEach(attachLongPressListeners);
}
});
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
function handleLongPress(event, link) {
event.preventDefault();
const playerId = new URL(link.href).searchParams.get('XID');
if (!playerId) return;
if (!scriptSettings.apiKey) {
showMenu(event.clientX || event.touches[0].clientX, event.clientY || event.touches[0].clientY, 'API key not set. Please configure in settings.');
return;
}
// Store player ID for when the popup appears
window.lastActionPendingPlayerId = playerId;
// Start watching for popup appearance with multiple detection methods
watchForQuickView(playerId);
}
function watchForQuickView(playerId) {
// Store the player ID globally for the miniprofile observer
window.lastActionPendingPlayerId = playerId;
// The actual injection will be handled by the miniprofile observer
// Start a timeout to clear the pending player ID after 5 seconds
setTimeout(() => {
if (window.lastActionPendingPlayerId === playerId) {
window.lastActionPendingPlayerId = null;
}
}, 5000);
}
function injectIntoMiniProfile(wrapper, playerId) {
// Skip if already injected
if (wrapper.querySelector('.last-action-info')) return;
console.log('Last Action Viewer: Found miniprofile wrapper, injecting for player', playerId);
// Create the container div
const containerDiv = document.createElement('div');
containerDiv.style.cssText = 'color: var(--default-color); font-size: 12px; line-height: 14px;';
// Create the last action div with Lugburz-style formatting
const lastActionDiv = document.createElement('div');
lastActionDiv.innerHTML = '<b>Last action:</b> <span class="last-action-info">Loading...</span>';
// Create the view more button (initially hidden)
const viewMoreBtn = document.createElement('div');
viewMoreBtn.style.cssText = 'cursor: pointer; color: #4CAF50; text-decoration: underline; margin-top: 2px; display: none;';
viewMoreBtn.textContent = 'View more';
// Create the expanded stats div (initially hidden)
const expandedDiv = document.createElement('div');
expandedDiv.style.cssText = 'margin-top: 4px; display: none; font-size: 11px; line-height: 13px; padding: 6px 8px; background: rgba(0,0,0,0.8); border-radius: 4px; border: 1px solid rgba(255,255,255,0.3);';
// Add click handler for view more
viewMoreBtn.addEventListener('click', () => {
if (expandedDiv.style.display === 'none') {
expandedDiv.style.display = 'block';
viewMoreBtn.textContent = 'View less';
} else {
expandedDiv.style.display = 'none';
viewMoreBtn.textContent = 'View more';
}
});
// Append elements to container
containerDiv.appendChild(lastActionDiv);
containerDiv.appendChild(viewMoreBtn);
containerDiv.appendChild(expandedDiv);
// Append container to wrapper
wrapper.appendChild(containerDiv);
// Fetch the API data with personalstats
const url = `https://api.torn.com/user/${playerId}?selections=profile,personalstats&key=${scriptSettings.apiKey}`;
fetchJSON(url, data => {
const span = lastActionDiv.querySelector('.last-action-info');
if (data && data.last_action) {
const lastAction = data.last_action;
span.textContent = lastAction.relative;
// Show view more button and populate expanded stats
if (data.personalstats) {
viewMoreBtn.style.display = 'block';
const stats = data.personalstats;
expandedDiv.innerHTML = `
<b>Xanax Taken:</b> ${stats.xantaken || 0}<br>
<b>Refills:</b> ${stats.refills || 0}<br>
<b>Attacks Won:</b> ${stats.attackswon || 0}<br>
<b>Defends Won:</b> ${stats.defendswon || 0}<br>
<b>Stat Enhancers Used:</b> ${stats.statenhancersused || 0}<br>
<b>Networth:</b> $${(stats.networth || 0).toLocaleString()}
`;
}
} else if (data && data.error) {
span.textContent = `Error: ${data.error.error}`;
span.style.color = '#f44336';
} else {
span.textContent = 'Failed to fetch';
span.style.color = '#f44336';
}
});
}
function showMenu(x, y, text) {
const menu = document.createElement('div');
menu.className = 'last-action-menu';
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
menu.textContent = text;
document.body.appendChild(menu);
function removeMenu() {
document.body.removeChild(menu);
document.removeEventListener('click', removeMenu);
}
document.addEventListener('click', removeMenu, { once: true });
}
addLastActionSettingsMenuItem();
initLastActionViewer();
// Lugburz-style miniprofile observer
const miniProfileObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
const newNodes = mutation.addedNodes;
if (newNodes) {
for (let i = 0; i < newNodes.length; i++) {
const node = newNodes[i];
if (node.nodeType === Node.ELEMENT_NODE) {
// Check for profile-mini-root
if (node.id && node.id === 'profile-mini-root') {
miniProfileObserver.disconnect();
miniProfileObserver.observe(node, { subtree: true, childList: true });
}
// Check for user profile wrapper (Lugburz style)
else if (node.className && node.className.indexOf && node.className.indexOf('profile-mini-_userProfileWrapper___') > -1) {
// Only inject if we have a pending player ID from long press
if (window.lastActionPendingPlayerId) {
const playerId = window.lastActionPendingPlayerId;
window.lastActionPendingPlayerId = null; // Clear it
injectIntoMiniProfile(node, playerId);
}
}
// Also check children for the wrapper class
else if (node.querySelectorAll) {
const wrappers = node.querySelectorAll('[class*="profile-mini-_userProfileWrapper___"]');
wrappers.forEach(wrapper => {
if (window.lastActionPendingPlayerId) {
const playerId = window.lastActionPendingPlayerId;
window.lastActionPendingPlayerId = null;
injectIntoMiniProfile(wrapper, playerId);
}
});
}
}
}
}
});
});
// Start observing the entire body for miniprofile elements
miniProfileObserver.observe(document.body, { subtree: true, childList: true });
const menuObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('settings-menu')) {
addLastActionSettingsMenuItem();
break;
}
}
}
});
});
menuObserver.observe(document.body, { childList: true, subtree: true });
})();