您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows player's last action on long-click, with API key management
// ==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 }); })();