您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a dark-mode 'Copy' button with a customizable settings menu next to a user's name.
当前为
// ==UserScript== // @name Torn Profile Link Formatter // @namespace GNSC4 [268863] // @version 1.3.0 // @description Adds a dark-mode 'Copy' button with a customizable settings menu next to a user's name. // @author GNSC4 [268863] // @match https://www.torn.com/profiles.php?XID=* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // --- GM Polyfills for environments without native support --- const GNSC_setValue = typeof GM_setValue !== 'undefined' ? GM_setValue : (key, value) => localStorage.setItem(key, JSON.stringify(value)); const GNSC_getValue = typeof GM_getValue !== 'undefined' ? GM_getValue : (key, def) => JSON.parse(localStorage.getItem(key)) || def; // --- Add Styles for the UI --- if (typeof GM_addStyle !== 'undefined') { GM_addStyle(` .gnsc-copy-container { display: inline-flex; align-items: center; vertical-align: middle; gap: 5px; margin-left: 10px; } .gnsc-btn { background-color: #333; color: #DDD; border: 1px solid #555; border-radius: 5px; padding: 3px 8px; text-decoration: none; font-size: 12px; line-height: 1.5; font-weight: bold; cursor: pointer; white-space: nowrap; } .gnsc-btn:hover { background-color: #444; } .gnsc-settings-panel { display: none; position: absolute; background-color: #2c2c2c; border: 1px solid #555; border-radius: 5px; padding: 10px; z-index: 1000; top: 100%; left: 0; min-width: 200px; } .gnsc-settings-panel div { margin-bottom: 5px; display: flex; align-items: center; } .gnsc-settings-panel label { color: #DDD; flex-grow: 1; } .gnsc-settings-panel input[type="checkbox"] { margin-left: 5px; } .gnsc-settings-panel label.disabled { color: #888; } .gnsc-settings-container { position: relative; } .gnsc-api-key-wrapper { display: flex; flex-direction: column; align-items: flex-start !important; } .gnsc-api-key-wrapper label { margin-bottom: 4px; } .gnsc-api-key-input-wrapper { display: flex; width: 100%; } .gnsc-api-key-input { width: 100%; background-color: #1e1e1e; border: 1px solid #555; color: #ddd; border-radius: 3px; padding: 2px 4px;} #gnsc-show-api-key-btn { font-size: 10px; margin-left: 4px; padding: 2px 4px; } `); } /** * Main script logic, called after elements are confirmed to exist. */ function main(nameElement, infoTable) { const urlParams = new URLSearchParams(window.location.search); const userId = urlParams.get('XID'); if (!userId) return; const userName = nameElement.textContent.split(' [')[0].trim(); let factionLinkEl = null; let companyLinkEl = null; let hospitalInfo = { inHospital: false, releaseTimestamp: 0 }; const infoListItems = infoTable.querySelectorAll('li'); infoListItems.forEach(item => { const titleEl = item.querySelector('.user-information-section .bold'); if (!titleEl) return; const title = titleEl.textContent.trim(); if (title === 'Faction') factionLinkEl = item.querySelector('.user-info-value a'); if (title === 'Job') companyLinkEl = item.querySelector('.user-info-value a'); }); const statusDescEl = document.querySelector('.profile-container .description .main-desc'); if (statusDescEl && statusDescEl.textContent.includes('In hospital')) { hospitalInfo.inHospital = true; const hospitalTimeStr = statusDescEl.textContent.trim(); const hourMatch = hospitalTimeStr.match(/(\d+)\s+hour(s?)/); const minMatch = hospitalTimeStr.match(/(\d+)\s+minute(s?)/); const secMatch = hospitalTimeStr.match(/(\d+)\s+second(s?)/); let totalSeconds = 0; if (hourMatch) totalSeconds += parseInt(hourMatch[1], 10) * 3600; if (minMatch) totalSeconds += parseInt(minMatch[1], 10) * 60; if (secMatch) totalSeconds += parseInt(secMatch[1], 10); hospitalInfo.releaseTimestamp = Date.now() + totalSeconds * 1000; } const userInfo = { id: userId, name: userName, profileUrl: `https://www.torn.com/profiles.php?XID=${userId}`, attackUrl: `https://www.torn.com/loader2.php?sid=getInAttack&user2ID=${userId}`, factionUrl: factionLinkEl ? factionLinkEl.href : null, companyUrl: companyLinkEl ? companyLinkEl.href : null, hospitalInfo: hospitalInfo }; createUI(nameElement, userInfo); } /** * Creates and injects all UI elements (buttons, panel). */ function createUI(targetElement, userInfo) { const container = document.createElement('div'); container.className = 'gnsc-copy-container'; const copyButton = document.createElement('a'); copyButton.href = "#"; copyButton.className = 'gnsc-btn'; copyButton.innerHTML = '<span>Copy</span>'; copyButton.addEventListener('click', (e) => handleCopyClick(e, copyButton, userInfo)); const settingsContainer = document.createElement('div'); settingsContainer.className = 'gnsc-settings-container'; const settingsButton = document.createElement('a'); settingsButton.href = "#"; settingsButton.className = 'gnsc-btn'; settingsButton.innerHTML = '⚙️'; const settingsPanel = createSettingsPanel(userInfo); settingsButton.addEventListener('click', (e) => { e.preventDefault(); settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block'; }); document.addEventListener('click', (e) => { if (!settingsContainer.contains(e.target)) { settingsPanel.style.display = 'none'; } }); settingsContainer.appendChild(settingsButton); settingsContainer.appendChild(settingsPanel); container.appendChild(copyButton); container.appendChild(settingsContainer); targetElement.insertAdjacentElement('afterend', container); } /** * Creates the settings panel element. */ function createSettingsPanel(userInfo) { const panel = document.createElement('div'); panel.className = 'gnsc-settings-panel'; const settings = loadSettings(); // API Key Section const apiKeyWrapper = document.createElement('div'); apiKeyWrapper.className = 'gnsc-api-key-wrapper'; apiKeyWrapper.innerHTML = `<label for="gnsc-api-key">TornStats API Key</label>`; const inputWrapper = document.createElement('div'); inputWrapper.className = 'gnsc-api-key-input-wrapper'; const apiKeyInput = document.createElement('input'); apiKeyInput.type = 'password'; apiKeyInput.id = 'gnsc-api-key'; apiKeyInput.className = 'gnsc-api-key-input'; apiKeyInput.value = settings.apiKey || ''; apiKeyInput.addEventListener('input', () => { updateBattleStatsAvailability(); saveSettings(); }); const showApiKeyBtn = document.createElement('button'); showApiKeyBtn.id = 'gnsc-show-api-key-btn'; showApiKeyBtn.className = 'gnsc-btn'; showApiKeyBtn.textContent = 'Show'; showApiKeyBtn.addEventListener('click', (e) => { e.preventDefault(); const isPassword = apiKeyInput.type === 'password'; apiKeyInput.type = isPassword ? 'text' : 'password'; showApiKeyBtn.textContent = isPassword ? 'Hide' : 'Show'; }); inputWrapper.appendChild(apiKeyInput); inputWrapper.appendChild(showApiKeyBtn); apiKeyWrapper.appendChild(inputWrapper); panel.appendChild(apiKeyWrapper); panel.appendChild(document.createElement('hr')); // Checkbox Options const options = [ { key: 'profile', label: 'Profile', available: true }, { key: 'attack', label: 'Attack', available: true }, { key: 'faction', label: 'Faction', available: !!userInfo.factionUrl }, { key: 'company', label: 'Company', available: !!userInfo.companyUrl }, { key: 'hospital', label: 'Hospital Time', available: userInfo.hospitalInfo.inHospital }, { key: 'battlestats', label: 'Battle Stats', available: true } // Always create the element ]; options.forEach(option => { const wrapper = document.createElement('div'); const checkbox = document.createElement('input'); const label = document.createElement('label'); checkbox.type = 'checkbox'; checkbox.id = `gnsc-check-${option.key}`; checkbox.checked = option.available && settings[option.key]; checkbox.disabled = !option.available; // Will be updated dynamically for battlestats checkbox.addEventListener('change', () => saveSettings()); label.htmlFor = `gnsc-check-${option.key}`; label.textContent = option.label; if (!option.available) label.classList.add('disabled'); wrapper.appendChild(label); wrapper.appendChild(checkbox); panel.appendChild(wrapper); }); updateBattleStatsAvailability(); // Set initial state of the Battle Stats checkbox return panel; } function formatNumber(num) { if (num < 1e3) return num; if (num >= 1e3 && num < 1e6) return +(num / 1e3).toFixed(2) + "K"; if (num >= 1e6 && num < 1e9) return +(num / 1e6).toFixed(2) + "M"; if (num >= 1e9 && num < 1e12) return +(num / 1e9).toFixed(2) + "B"; if (num >= 1e12 && num < 1e15) return +(num / 1e12).toFixed(2) + "T"; if (num >= 1e15) return +(num / 1e15).toFixed(2) + "Q"; }; function formatTimeDifference(timestamp) { const now = Math.floor(Date.now() / 1000); const seconds = now - timestamp; let interval = seconds / 31536000; if (interval > 1) return Math.floor(interval) + " years ago"; interval = seconds / 2592000; if (interval > 1) return Math.floor(interval) + " months ago"; interval = seconds / 86400; if (interval > 1) return Math.floor(interval) + " days ago"; interval = seconds / 3600; if (interval > 1) return Math.floor(interval) + " hours ago"; interval = seconds / 60; if (interval > 1) return Math.floor(interval) + " minutes ago"; return Math.floor(seconds) + " seconds ago"; } async function handleCopyClick(e, button, userInfo) { e.preventDefault(); const settings = loadSettings(); let battleStatsStr = null; let hospitalStr = null; if (settings.hospital && userInfo.hospitalInfo.inHospital) { const currentStatusDescEl = document.querySelector('.profile-container .description .main-desc'); const relativeTimeStr = currentStatusDescEl ? currentStatusDescEl.textContent.trim() : ''; const releaseDate = new Date(userInfo.hospitalInfo.releaseTimestamp); const tctTimeString = releaseDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: 'UTC' }); hospitalStr = `(${relativeTimeStr} | Out at ${tctTimeString} TCT)`; } if (settings.battlestats && settings.apiKey) { button.innerHTML = '<span>Fetching...</span>'; try { const spyData = await fetchTornStatsSpy(settings.apiKey, userInfo.id); const spyResult = spyData?.spy; if (spyResult?.status === true && typeof spyResult?.total !== 'undefined') { const str = formatNumber(spyResult.strength); const def = formatNumber(spyResult.defense); const spd = formatNumber(spyResult.speed); const dex = formatNumber(spyResult.dexterity); const total = formatNumber(spyResult.total); const timeAgo = formatTimeDifference(spyResult.timestamp); battleStatsStr = `(Str: ${str} | Def: ${def} | Spd: ${spd} | Dex: ${dex} | Total: ${total} | Spy: ${timeAgo})`; } else { battleStatsStr = "(Stats: N/A)"; } } catch (error) { console.error("Torn Profile Link Formatter: Failed to fetch TornStats data.", error); battleStatsStr = "(Stats: API Error)"; } } const links = []; if (settings.profile) links.push(`<a href="${userInfo.profileUrl}">Profile</a>`); if (settings.attack) links.push(`<a href="${userInfo.attackUrl}">Attack</a>`); if (settings.faction && userInfo.factionUrl) links.push(`<a href="${userInfo.factionUrl}">Faction</a>`); if (settings.company && userInfo.companyUrl) links.push(`<a href="${userInfo.companyUrl}">Company</a>`); if (hospitalStr) links.push(hospitalStr); if (battleStatsStr) links.push(battleStatsStr); const formattedString = links.length > 0 ? `${userInfo.name} - ${links.join(' - ')}` : userInfo.name; copyToClipboard(formattedString); const originalText = '<span>Copy</span>'; button.innerHTML = '<span>Copied!</span>'; button.style.backgroundColor = '#2a633a'; setTimeout(() => { button.innerHTML = originalText; button.style.backgroundColor = ''; }, 2000); } function fetchTornStatsSpy(apiKey, userId) { const requestUrl = `https://www.tornstats.com/api/v2/${apiKey}/spy/user/${userId}`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: requestUrl, onload: function(response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch(e) { reject(new Error("Failed to parse JSON from TornStats.")); } } else { reject(new Error(`API responded with status ${response.status}`)); } }, onerror: function(error) { reject(error); } }); }); } function loadSettings() { return GNSC_getValue('tornProfileFormatterSettings', { profile: true, attack: true, faction: false, company: false, hospital: true, battlestats: false, apiKey: '' }); } /** * Updates the availability of the Battle Stats option based on the API key input. */ function updateBattleStatsAvailability() { const battleStatsCheckbox = document.getElementById('gnsc-check-battlestats'); const battleStatsLabel = document.querySelector('label[for="gnsc-check-battlestats"]'); const apiKeyInput = document.getElementById('gnsc-api-key'); if (!battleStatsCheckbox || !battleStatsLabel || !apiKeyInput) return; const hasApiKey = !!apiKeyInput.value.trim(); battleStatsCheckbox.disabled = !hasApiKey; battleStatsLabel.classList.toggle('disabled', !hasApiKey); if (!hasApiKey) { battleStatsCheckbox.checked = false; } } function saveSettings() { const battleStatsCheckbox = document.getElementById('gnsc-check-battlestats'); const apiKeyInput = document.getElementById('gnsc-api-key'); const hasApiKey = !!(apiKeyInput && apiKeyInput.value.trim()); const settings = { profile: document.getElementById('gnsc-check-profile').checked, attack: document.getElementById('gnsc-check-attack').checked, faction: document.getElementById('gnsc-check-faction')?.checked || false, company: document.getElementById('gnsc-check-company')?.checked || false, hospital: document.getElementById('gnsc-check-hospital')?.checked || false, battlestats: hasApiKey && battleStatsCheckbox?.checked || false, apiKey: apiKeyInput?.value || '' }; GNSC_setValue('tornProfileFormatterSettings', settings); } function copyToClipboard(text) { const tempTextarea = document.createElement('textarea'); tempTextarea.style.position = 'fixed'; tempTextarea.style.left = '-9999px'; tempTextarea.value = text; document.body.appendChild(tempTextarea); tempTextarea.select(); document.execCommand('copy'); document.body.removeChild(tempTextarea); } const observer = new MutationObserver((mutations, obs) => { const nameElement = document.querySelector('#skip-to-content'); const infoTable = document.querySelector('.basic-information .info-table'); const alreadyInjected = document.querySelector('.gnsc-copy-container'); if (nameElement && infoTable && infoTable.children.length > 5 && !alreadyInjected) { main(nameElement, infoTable); obs.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); })();