您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays effects of items in Torn's various item shops with hover tooltips and disables items with warnings when toggle is active.
// ==UserScript== // @name Torn Item Safety // @namespace http://tampermonkey.net/ // @version 1.1.1 // @license GNU GPLv3 // @description Displays effects of items in Torn's various item shops with hover tooltips and disables items with warnings when toggle is active. // @author Vassilios [2276659] // @match https://www.torn.com/item.php* // @match https://www.torn.com/bigalgunshop.php* // @match https://www.torn.com/shops.php?step=* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect api.torn.com // ==/UserScript== (function() { 'use strict'; // Register Tampermonkey menu command to enter API key GM_registerMenuCommand('Enter API key.', () => { const newApiKey = prompt('Please enter your Torn API key:', ''); if (newApiKey && newApiKey.trim() !== '') { localStorage.setItem(CONFIG.STORAGE_KEYS.API_KEY, newApiKey.trim()); // Clear cached data to force a fresh fetch with the new key localStorage.removeItem(CONFIG.STORAGE_KEYS.ITEM_DATA); localStorage.removeItem(CONFIG.STORAGE_KEYS.LAST_REQUEST_TIME); // Trigger a refresh of item data ItemEffectsApp.init(); } }); // ===================================================================== // CONFIGURATION // ===================================================================== const CONFIG = { getApiKey: () => localStorage.getItem('tornItemEffects_apiKey') || "", // Dynamically retrieve API key API_BASE_URL: 'https://api.torn.com/v2/torn/items', ITEM_CATEGORIES: ['Tool', 'Enhancer'], CACHE_DURATION: 24 * 60 * 60 * 1000, // 24 hours in milliseconds OBSERVER_CONFIG: { childList: true, subtree: true }, ELEMENT_IDS: { WARNINGS_TOGGLE: 'warnings-toggle', WARNINGS_STATUS: 'warnings-status' }, SELECTORS: { ALL_ITEMS: '#all-items', SELL_ITEMS_WRAP: '.sell-items-wrap', CONTENT_TITLE_LINKS: '.content-title-links', ITEM_NAME: '.name', WARNING_SIGN: '.warning-sign' }, STORAGE_KEYS: { WARNING_STATE: 'tornItemEffects_warningState', ITEM_DATA: 'tornItemEffects_itemData', LAST_REQUEST_TIME: 'tornItemEffects_lastRequestTime', API_KEY: 'tornItemEffects_apiKey' }, STYLES: { WARNING_SIGN: { color: '#ff9900', fontWeight: 'bold', marginLeft: '5px', cursor: 'help' }, TOOLTIP: { position: 'absolute', backgroundColor: '#191919', color: '#F7FAFC', padding: '6px 10px', borderRadius: '4px', fontSize: '12px', zIndex: '9999999', display: 'none', boxShadow: '0 1px 3px rgba(0,0,0,0.2)', maxWidth: '250px', textAlign: 'left', fontFamily: 'Segoe UI, Arial, sans-serif', lineHeight: '1.3', border: '1px solid #4A5568', wordWrap: 'break-word', overflowWrap: 'break-word' }, STATUS_INDICATOR: { ON: { text: 'ON', color: '#4CAF50' }, OFF: { text: 'OFF', color: '#F44336' } } } }; // ===================================================================== // STATE MANAGEMENT // ===================================================================== const State = { itemData: [], observers: { items: null, body: null, disabledInputs: null }, disabledInputs: new Map() // Store references to disabled inputs }; // ===================================================================== // API INTERFACE // ===================================================================== const ApiService = { fetchItemCategory: function(category) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest === 'undefined') { console.error('GM_xmlhttpRequest is not available'); reject(new Error('GM_xmlhttpRequest is not defined')); return; } const apiKey = CONFIG.getApiKey(); const url = `${CONFIG.API_BASE_URL}?cat=${category}&sort=ASC`; GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'accept': 'application/json', 'Authorization': `ApiKey ${apiKey}` }, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data && data.items) { resolve(data.items); } else { console.error('API response does not contain items:', response.responseText); reject(new Error('Invalid data format from API')); } } catch (error) { console.error('Error parsing API response:', response.responseText, error); reject(error); } }, onerror: function(error) { console.error('GM_xmlhttpRequest error:', error); reject(error); } }); }); }, fetchAllItemData: function() { // Check if API key is set const apiKey = CONFIG.getApiKey(); if (!apiKey) { const cachedData = localStorage.getItem(CONFIG.STORAGE_KEYS.ITEM_DATA); if (cachedData) { try { State.itemData = JSON.parse(cachedData); return Promise.resolve(State.itemData); } catch (error) { console.error('Error parsing cached data:', error); return Promise.resolve([]); } } return Promise.resolve([]); } // Check for cached data const cachedData = localStorage.getItem(CONFIG.STORAGE_KEYS.ITEM_DATA); const lastRequestTime = localStorage.getItem(CONFIG.STORAGE_KEYS.LAST_REQUEST_TIME); const currentTime = Date.now(); if (cachedData && lastRequestTime && cachedData !== "[]") { const timeSinceLastRequest = currentTime - parseInt(lastRequestTime, 10); if (timeSinceLastRequest < CONFIG.CACHE_DURATION) { try { State.itemData = JSON.parse(cachedData); return Promise.resolve(State.itemData); } catch (error) { console.error('Error parsing cached data:', error); } } } // Fetch new data const fetchPromises = CONFIG.ITEM_CATEGORIES.map(category => this.fetchItemCategory(category) .then(items => { const simplifiedItems = items.map(item => ({ name: item.name, effect: item.effect })); State.itemData = [...State.itemData, ...simplifiedItems]; return simplifiedItems; }) .catch(error => { console.error(`Error fetching ${category} items:`, error); return []; }) ); return Promise.all(fetchPromises).then(() => { // Save to localStorage try { localStorage.setItem(CONFIG.STORAGE_KEYS.ITEM_DATA, JSON.stringify(State.itemData)); localStorage.setItem(CONFIG.STORAGE_KEYS.LAST_REQUEST_TIME, currentTime.toString()); } catch (error) { console.error('Error saving to localStorage:', error); } return State.itemData; }); } }; // ===================================================================== // DOM UTILITIES // ===================================================================== const DomUtils = { find: { itemContainers: function() { const containers = []; const allItemsList = document.querySelector(CONFIG.SELECTORS.ALL_ITEMS); if (allItemsList) containers.push(allItemsList); const sellItemsWrap = document.querySelector(CONFIG.SELECTORS.SELL_ITEMS_WRAP); if (sellItemsWrap) containers.push(sellItemsWrap); return containers; }, listItems: function() { const containers = this.itemContainers(); let items = []; containers.forEach(container => { const containerItems = Array.from(container.getElementsByTagName('li')); items = [...items, ...containerItems]; }); return items; }, navigationContainer: function() { return document.querySelector(CONFIG.SELECTORS.CONTENT_TITLE_LINKS); } }, create: { warningSign: function(effectText) { const warningSign = document.createElement('span'); warningSign.className = 'warning-sign'; Object.assign(warningSign.style, CONFIG.STYLES.WARNING_SIGN); warningSign.innerHTML = '⚠️'; const tooltip = this.tooltip(effectText); warningSign.appendChild(tooltip); warningSign.addEventListener('mouseenter', function() { tooltip.style.display = 'block'; const rect = warningSign.getBoundingClientRect(); const isLongText = (tooltip.textContent || '').length > 50; tooltip.style.left = '0px'; tooltip.style.top = isLongText ? '-45px' : '-30px'; setTimeout(() => { const tooltipRect = tooltip.getBoundingClientRect(); if (tooltipRect.left < 0) tooltip.style.left = '5px'; if (tooltipRect.top < 0) tooltip.style.top = '20px'; }, 0); }); warningSign.addEventListener('mouseleave', function() { tooltip.style.display = 'none'; }); return warningSign; }, tooltip: function(content) { const tooltip = document.createElement('div'); tooltip.className = 'item-effect-tooltip'; tooltip.setAttribute('role', 'tooltip'); Object.assign(tooltip.style, CONFIG.STYLES.TOOLTIP); tooltip.textContent = content; return tooltip; }, toggleButton: function() { const toggleButton = document.createElement('a'); toggleButton.id = CONFIG.ELEMENT_IDS.WARNINGS_TOGGLE; toggleButton.className = 'warnings-toggle t-clear h c-pointer m-icon line-h24 right'; toggleButton.setAttribute('aria-labelledby', 'warnings-toggle-label'); const savedState = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE); const isActive = savedState !== null ? savedState === 'true' : true; if (isActive) { toggleButton.classList.add('top-page-link-button--active'); toggleButton.classList.add('active'); } toggleButton.innerHTML = ` <span class="icon-wrap svg-icon-wrap"> <span class="link-icon-svg warnings-icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 16"> <defs> <style>.cls-1{opacity:0.35;}.cls-2{fill:#fff;}.cls-3{fill:#777;}</style> </defs> <g> <g> <g class="cls-1"> <path class="cls-2" d="M7.5,1 L15,15 L0,15 Z"></path> </g> <path class="cls-3" d="M7.5,0 L15,14 L0,14 Z"></path> <path class="cls-3" d="M7,6 L8,6 L8,10 L7,10 Z" style="fill:#fff"></path> <circle class="cls-3" cx="7.5" cy="12" r="1" style="fill:#fff"></circle> </g> </g> </svg> </span> </span> <span id="warnings-toggle-label">Item Safety:</span> `; const statusIndicator = document.createElement('span'); statusIndicator.id = CONFIG.ELEMENT_IDS.WARNINGS_STATUS; statusIndicator.style.marginLeft = '5px'; statusIndicator.style.fontWeight = 'bold'; statusIndicator.textContent = isActive ? CONFIG.STYLES.STATUS_INDICATOR.ON.text : CONFIG.STYLES.STATUS_INDICATOR.OFF.text; statusIndicator.style.color = isActive ? CONFIG.STYLES.STATUS_INDICATOR.ON.color : CONFIG.STYLES.STATUS_INDICATOR.OFF.color; toggleButton.appendChild(statusIndicator); return toggleButton; } } }; // ===================================================================== // INPUT PROTECTION FUNCTIONALITY // ===================================================================== const InputProtection = { setupDisabledInputsObserver: function() { // Create a MutationObserver to watch for changes to disabled inputs const observerConfig = { attributes: true, attributeFilter: ['disabled', 'value'], subtree: true }; State.observers.disabledInputs = new MutationObserver(mutations => { mutations.forEach(mutation => { const element = mutation.target; // Check if warnings are enabled before applying protection const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false'; if (!warningsEnabled) return; // Handle disabled attribute changes if (mutation.attributeName === 'disabled') { if (!element.disabled && element.dataset.disabledByWarning === 'true') { // Element was disabled by our script but something tried to enable it // Re-disable it element.disabled = true; } } // Handle value changes on disabled inputs if (mutation.attributeName === 'value' && element.disabled && element.dataset.disabledByWarning === 'true') { // Reset value to 0 if it was changed while disabled if (element.value !== '0') { element.value = '0'; } } }); }); document.addEventListener('input', function(e) { // Check if warnings are enabled before applying protection const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false'; if (!warningsEnabled) return; // For any input events on disabled inputs if (e.target.disabled && e.target.dataset.disabledByWarning === 'true') { e.preventDefault(); e.stopPropagation(); e.target.value = '0'; } }, true); // Start observing the entire document State.observers.disabledInputs.observe(document.documentElement, observerConfig); }, // Proxy for input properties to intercept changes to disabled inputs setupInputPropertyProxy: function() { // Store original property descriptors const originalValueDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); const originalDisabledDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'disabled'); // Override the value property Object.defineProperty(HTMLInputElement.prototype, 'value', { set: function(newValue) { // Check if warnings are enabled before applying protection const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false'; if (warningsEnabled && this.disabled && this.dataset.disabledByWarning === 'true') { originalValueDescriptor.set.call(this, '0'); return '0'; } else { return originalValueDescriptor.set.call(this, newValue); } }, get: function() { return originalValueDescriptor.get.call(this); }, configurable: true }); // Override the disabled property Object.defineProperty(HTMLInputElement.prototype, 'disabled', { set: function(value) { // Check if warnings are enabled before applying protection const warningsEnabled = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE) !== 'false'; if (warningsEnabled && !value && this.dataset.disabledByWarning === 'true') { return originalDisabledDescriptor.set.call(this, true); } else { return originalDisabledDescriptor.set.call(this, value); } }, get: function() { return originalDisabledDescriptor.get.call(this); }, configurable: true }); }, // Method to track new disabled inputs trackDisabledInput: function(input) { if (input.type === 'text' || input.type === 'number') { // Store original value input.dataset.originalValue = input.value || ''; // Set value to 0 for numerical inputs if (input.type === 'number' || !isNaN(parseFloat(input.value))) { input.value = '0'; } } // Add to our tracking map State.disabledInputs.set(input, { originalDisabled: input.disabled, originalValue: input.dataset.originalValue }); }, // Method to untrack and restore inputs when warnings are disabled restoreInput: function(input) { // Restore original value if it exists if (input.dataset.originalValue !== undefined) { input.value = input.dataset.originalValue; delete input.dataset.originalValue; } // Restore original state input.disabled = false; input.style.opacity = ''; input.style.cursor = ''; delete input.dataset.disabledByWarning; if (input.type === 'text' && input.dataset.originalBg !== undefined) { input.style.backgroundColor = input.dataset.originalBg; delete input.dataset.originalBg; } // Remove from tracking State.disabledInputs.delete(input); }, // Initialize input protection init: function() { this.setupDisabledInputsObserver(); this.setupInputPropertyProxy(); } }; // ===================================================================== // CORE FUNCTIONALITY // ===================================================================== const ItemEffectsApp = { init: function() { this.initializeToggleButton(); // Initialize input protection first InputProtection.init(); ApiService.fetchAllItemData() .then(() => { this.processItems(); this.setupObservers(); this.applyWarningState(); // Changed from applyInitialWarningState to be more general }) .catch(error => { console.error('Failed to fetch item data:', error); this.processItems(); this.setupObservers(); this.applyWarningState(); // Changed from applyInitialWarningState to be more general }); }, applyWarningState: function() { const savedState = localStorage.getItem(CONFIG.STORAGE_KEYS.WARNING_STATE); const isActive = savedState !== null ? savedState === 'true' : true; const warningElements = document.querySelectorAll(CONFIG.SELECTORS.WARNING_SIGN); warningElements.forEach(warning => { const listItem = warning.closest('li[data-item]'); if (!listItem) return; const isItemPage = window.location.href.includes('item.php'); if (isItemPage) { const deleteButtons = listItem.querySelectorAll('.option-delete'); deleteButtons.forEach(button => { if (isActive) { button.disabled = true; button.style.opacity = '0.5'; button.style.cursor = 'not-allowed'; button.dataset.disabledByWarning = 'true'; } else { button.disabled = false; button.style.opacity = ''; button.style.cursor = ''; delete button.dataset.disabledByWarning; } }); } else { const inputElements = listItem.querySelectorAll('input, button, select, textarea, a.buy'); inputElements.forEach(input => { if (isActive) { if (input.tagName.toLowerCase() === 'a') { input.dataset.originalHref = input.href; input.href = 'javascript:void(0)'; input.style.opacity = '0.5'; input.style.pointerEvents = 'none'; } else { // Store original value before disabling if (input.type === 'text' || input.type === 'number') { input.dataset.originalValue = input.value || ''; } input.disabled = true; input.style.opacity = '0.5'; input.style.cursor = 'not-allowed'; input.dataset.disabledByWarning = 'true'; // Track and protect this disabled input InputProtection.trackDisabledInput(input); if (input.type === 'text') { input.dataset.originalBg = input.style.backgroundColor; input.style.backgroundColor = '#e0e0e0'; } } } else { if (input.tagName.toLowerCase() === 'a') { if (input.dataset.originalHref) { input.href = input.dataset.originalHref; } input.style.opacity = ''; input.style.pointerEvents = ''; } else { // Use the dedicated restore method InputProtection.restoreInput(input); } } }); } }); }, processItems: function() { const listItems = DomUtils.find.listItems(); if (listItems.length === 0) return; listItems.forEach(item => this.processItemElement(item)); // After processing items, apply the warning state based on toggle setting setTimeout(() => this.applyWarningState(), 0); }, processItemElement: function(itemElement) { const nameElement = itemElement.querySelector(CONFIG.SELECTORS.ITEM_NAME); if (!nameElement || !nameElement.textContent) return; const itemName = nameElement.textContent.trim(); const matchingItem = State.itemData.find(item => item.name === itemName); if (matchingItem && matchingItem.effect && !nameElement.querySelector(CONFIG.SELECTORS.WARNING_SIGN)) { const warningSign = DomUtils.create.warningSign(matchingItem.effect); nameElement.appendChild(warningSign); } }, initializeToggleButton: function() { if (document.getElementById(CONFIG.ELEMENT_IDS.WARNINGS_TOGGLE)) { return; } const navContainer = DomUtils.find.navigationContainer(); if (!navContainer) { if (document.readyState !== 'complete' && document.readyState !== 'interactive') { document.addEventListener('DOMContentLoaded', () => this.initializeToggleButton()); } return; } const toggleButton = DomUtils.create.toggleButton(); toggleButton.addEventListener('click', this.toggleWarnings); navContainer.appendChild(toggleButton); }, toggleWarnings: function() { this.classList.toggle('top-page-link-button--active'); this.classList.toggle('active'); const warningsEnabled = this.classList.contains('active'); localStorage.setItem(CONFIG.STORAGE_KEYS.WARNING_STATE, warningsEnabled); const statusIndicator = document.getElementById(CONFIG.ELEMENT_IDS.WARNINGS_STATUS); statusIndicator.textContent = warningsEnabled ? CONFIG.STYLES.STATUS_INDICATOR.ON.text : CONFIG.STYLES.STATUS_INDICATOR.OFF.text; statusIndicator.style.color = warningsEnabled ? CONFIG.STYLES.STATUS_INDICATOR.ON.color : CONFIG.STYLES.STATUS_INDICATOR.OFF.color; // Use the general applyWarningState method instead of duplicating logic here ItemEffectsApp.applyWarningState(); }, setupObservers: function() { const itemContainers = DomUtils.find.itemContainers(); State.observers.items = new MutationObserver(mutations => { let newItemsAdded = false; mutations.forEach(mutation => { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { newItemsAdded = true; } }); if (newItemsAdded) { this.processItems(); // Apply warning state after new items are processed this.applyWarningState(); } }); if (itemContainers.length > 0) { itemContainers.forEach(container => { State.observers.items.observe(container, CONFIG.OBSERVER_CONFIG); }); } else { this.setupBodyObserver(); } }, setupBodyObserver: function() { State.observers.body = new MutationObserver(mutations => { const itemContainers = DomUtils.find.itemContainers(); if (itemContainers.length > 0) { itemContainers.forEach(container => { State.observers.items.observe(container, CONFIG.OBSERVER_CONFIG); }); this.processItems(); this.initializeToggleButton(); // Apply warning state after container is found this.applyWarningState(); State.observers.body.disconnect(); } }); State.observers.body.observe(document.body, CONFIG.OBSERVER_CONFIG); } }; // ===================================================================== // INITIALIZATION // ===================================================================== if (document.readyState === 'complete' || document.readyState === 'interactive') { ItemEffectsApp.init(); } else { document.addEventListener('DOMContentLoaded', () => ItemEffectsApp.init()); } window.TornItemEffects = { processItems: () => ItemEffectsApp.processItems(), toggleWarnings: () => { const warningToggleButton = document.getElementById(CONFIG.ELEMENT_IDS.WARNINGS_TOGGLE); if (warningToggleButton) warningToggleButton.click(); } }; })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址