您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlight mods that have updated since you last downloaded them
// ==UserScript== // @name Nexus Mods - Updated Mod Highlighter // @version 2.0.0 // @description Highlight mods that have updated since you last downloaded them // @author Journey Over // @license MIT // @match *://www.nexusmods.com/* // @require https://cdn.jsdelivr.net/gh/StylusThemes/Userscripts@c185c2777d00a6826a8bf3c43bbcdcfeba5a9566/libs/utils/utils.min.js // @grant none // @icon https://www.google.com/s2/favicons?sz=64&domain=nexusmods.com // @homepageURL https://github.com/StylusThemes/Userscripts // @namespace https://gf.qytechs.cn/users/32214 // ==/UserScript== (function() { 'use strict'; // ============================================================================ // CONFIGURATION // ============================================================================ /** * Application configuration object containing all customizable settings * @type {Object} */ const CONFIG = { /** Table highlighting configuration */ table: { highlightClass: 'nexus-updated-mod-highlight', highlightColor: 'rgba(0,180,255,0.1)', // Electric blue }, /** Tile highlighting configuration */ tile: { styleId: 'nm-highlighter-style', updateClass: 'nm-update-card', downloadClass: 'nm-downloaded-card', /** Color schemes for different highlight types */ colors: { update: { primary: 'rgba(0,180,255,0.8)', // Electric blue secondary: 'rgba(0,240,255,0.6)', // Cyan glow: 'rgba(0,180,255,0.4)', bg: 'rgba(0,180,255,0.05)' }, download: { primary: 'rgba(180,0,255,0.8)', // Electric purple secondary: 'rgba(255,0,180,0.6)', // Magenta glow: 'rgba(180,0,255,0.4)', bg: 'rgba(180,0,255,0.05)' } }, /** CSS selectors for different tile types */ selectors: [ '[data-e2eid="mod-tile"]', '[data-e2eid="mod-tile-list"]', '[data-e2eid="mod-tile-standard"]', '[data-e2eid="mod-tile-compact"]', '[data-e2eid="mod-tile-teaser"]', '[class*="group/mod-tile"]' ], }, /** Global style configuration */ global: { styleId: 'nexus-global-style', }, /** Performance settings */ debounceDelay: 100, }; // ============================================================================ // CONSTANTS // ============================================================================ /** Animation durations in seconds */ const ANIMATION_DURATIONS = { TILE_GLOW: 2, TILE_PULSE: 2.5, TABLE_GLOW: 3, TABLE_STRIPE: 4, GRADIENT_SHIFT: 3, }; /** CSS selectors for page detection */ const PAGE_SELECTORS = { DOWNLOAD_HISTORY: { path: '/users/myaccount', tab: 'tab=download+history' } }; // ============================================================================ // GLOBAL VARIABLES // ============================================================================ /** Logger instance for debugging */ const logger = Logger('Nexus Mod - Updated Mod Highlighter', { debug: false }); /** MutationObserver for dynamic content changes */ let mutationObserver = null; // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ /** * Parses date strings into timestamps * @param {string} text - Date string to parse * @returns {number} Timestamp or NaN if invalid */ function parseDate(text) { if (!text) return NaN; const cleaned = text.replace(/\s+/g, ' ').trim(); const parsed = Date.parse(cleaned); return isNaN(parsed) ? new Date(cleaned).getTime() || NaN : parsed; } /** * Checks if current page is the download history page * @returns {boolean} True if on download history page */ function isDownloadHistoryPage() { return window.location.pathname.includes(PAGE_SELECTORS.DOWNLOAD_HISTORY.path) && window.location.search.includes(PAGE_SELECTORS.DOWNLOAD_HISTORY.tab); } /** * Generates combined tile selector string * @returns {string} Combined CSS selector */ function getTileSelector() { return CONFIG.tile.selectors.join(', '); } /** * Creates and injects a style element if it doesn't exist * @param {string} id - Style element ID * @param {string} css - CSS content * @param {string} description - Description for logging */ function injectStyleElement(id, css, description) { if (document.getElementById(id)) return; const style = document.createElement('style'); style.id = id; style.textContent = css; document.head.appendChild(style); logger.debug(`Injected ${description}`); } // ============================================================================ // STYLE INJECTION FUNCTIONS // ============================================================================ /** * Injects all required CSS styles */ function injectStyles() { injectTableStyles(); injectTileStyles(); injectGlobalStyles(); } /** * Injects table highlighting styles */ function injectTableStyles() { const css = ` @keyframes table-row-glow { 0%, 100% { box-shadow: inset 0 0 8px rgba(0,180,255,0.1), 0 0 4px rgba(0,180,255,0.2); background: linear-gradient(90deg, rgba(0,180,255,0.05) 0%, rgba(0,180,255,0.08) 50%, rgba(0,180,255,0.05) 100%); } 50% { box-shadow: inset 0 0 12px rgba(0,180,255,0.15), 0 0 8px rgba(0,180,255,0.3); background: linear-gradient(90deg, rgba(0,180,255,0.08) 0%, rgba(0,180,255,0.12) 50%, rgba(0,180,255,0.08) 100%); } } @keyframes table-stripe { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } .${CONFIG.table.highlightClass} { position: relative; animation: table-row-glow ${ANIMATION_DURATIONS.TABLE_GLOW}s ease-in-out infinite; background: linear-gradient(90deg, rgba(0,180,255,0.03) 0%, rgba(0,180,255,0.06) 50%, rgba(0,180,255,0.03) 100%); background-size: 200% 100%; transition: all 0.3s ease; } .${CONFIG.table.highlightClass}::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, transparent 0%, rgba(0,180,255,0.1) 20%, rgba(0,180,255,0.2) 50%, rgba(0,180,255,0.1) 80%, transparent 100%); background-size: 200% 100%; animation: table-stripe ${ANIMATION_DURATIONS.TABLE_STRIPE}s linear infinite; pointer-events: none; z-index: 1; } .${CONFIG.table.highlightClass}::after { content: ''; position: absolute; top: 0; left: 0; bottom: 0; width: 3px; background: linear-gradient(180deg, rgba(0,180,255,0.8) 0%, rgba(0,240,255,0.6) 50%, rgba(0,180,255,0.8) 100%); box-shadow: 0 0 8px rgba(0,180,255,0.4); z-index: 2; } .${CONFIG.table.highlightClass} td { position: relative; z-index: 3; color: inherit !important; font-weight: 500; transition: color 0.3s ease; } .${CONFIG.table.highlightClass}:hover { animation-duration: 1.5s; transform: translateY(-1px); } .${CONFIG.table.highlightClass}:hover::before { animation-duration: 2s; } `; injectStyleElement('nexus-updated-style', css, 'enhanced table styles'); } /** * Injects tile highlighting styles */ function injectTileStyles() { const css = ` @keyframes nm-glow { 0%, 100% { box-shadow: 0 0 8px ${CONFIG.tile.colors.update.glow}, 0 0 16px ${CONFIG.tile.colors.update.glow.replace('0.4', '0.2')}, 0 0 24px ${CONFIG.tile.colors.update.glow.replace('0.4', '0.1')}, inset 0 0 8px ${CONFIG.tile.colors.update.glow.replace('0.4', '0.1')}; filter: brightness(1.05) saturate(1.1); } 50% { box-shadow: 0 0 12px ${CONFIG.tile.colors.update.primary.replace('0.8', '0.6')}, 0 0 24px ${CONFIG.tile.colors.update.primary.replace('0.8', '0.4')}, 0 0 36px ${CONFIG.tile.colors.update.primary.replace('0.8', '0.2')}, inset 0 0 12px ${CONFIG.tile.colors.update.primary.replace('0.8', '0.15')}; filter: brightness(1.08) saturate(1.15); } } @keyframes nm-download-pulse { 0%, 100% { box-shadow: 0 0 6px ${CONFIG.tile.colors.download.glow}, 0 0 12px ${CONFIG.tile.colors.download.glow.replace('0.4', '0.2')}, inset 0 0 6px ${CONFIG.tile.colors.download.glow.replace('0.4', '0.05')}; } 50% { box-shadow: 0 0 10px ${CONFIG.tile.colors.download.primary.replace('0.8', '0.5')}, 0 0 20px ${CONFIG.tile.colors.download.primary.replace('0.8', '0.3')}, inset 0 0 10px ${CONFIG.tile.colors.download.primary.replace('0.8', '0.08')}; } } .${CONFIG.tile.updateClass} { position: relative; background: linear-gradient(135deg, ${CONFIG.tile.colors.update.bg} 0%, ${CONFIG.tile.colors.update.bg.replace('0.05', '0.03')} 50%, ${CONFIG.tile.colors.update.bg.replace('0.05', '0.01')} 100%); border: 2px solid transparent; border-image: linear-gradient(135deg, ${CONFIG.tile.colors.update.primary} 0%, ${CONFIG.tile.colors.update.secondary} 50%, ${CONFIG.tile.colors.update.primary.replace('0.8', '0.4')} 100%); border-image-slice: 1; animation: nm-glow ${ANIMATION_DURATIONS.TILE_GLOW}s ease-in-out infinite; transform: scale(1.02); transition: all 0.3s ease; } .${CONFIG.tile.updateClass}::before { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; background: linear-gradient(45deg, transparent 0%, ${CONFIG.tile.colors.update.bg.replace('0.05', '0.1')} 25%, ${CONFIG.tile.colors.update.bg.replace('0.05', '0.2')} 50%, ${CONFIG.tile.colors.update.bg.replace('0.05', '0.1')} 75%, transparent 100%); background-size: 200% 200%; animation: gradient-shift ${ANIMATION_DURATIONS.GRADIENT_SHIFT}s ease-in-out infinite; pointer-events: none; z-index: -1; } .${CONFIG.tile.downloadClass} { position: relative; background: linear-gradient(135deg, ${CONFIG.tile.colors.download.bg} 0%, ${CONFIG.tile.colors.download.bg.replace('0.05', '0.03')} 50%, ${CONFIG.tile.colors.download.bg.replace('0.05', '0.01')} 100%); border: 2px solid transparent; border-image: linear-gradient(135deg, ${CONFIG.tile.colors.download.primary} 0%, ${CONFIG.tile.colors.download.secondary} 50%, ${CONFIG.tile.colors.download.primary.replace('0.8', '0.3')} 100%); border-image-slice: 1; animation: nm-download-pulse ${ANIMATION_DURATIONS.TILE_PULSE}s ease-in-out infinite; transform: scale(1.01); transition: all 0.3s ease; } .${CONFIG.tile.downloadClass}::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(circle at center, ${CONFIG.tile.colors.download.bg.replace('0.05', '0.03')} 0%, transparent 70%); pointer-events: none; z-index: -1; } @keyframes gradient-shift { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } } `; injectStyleElement(CONFIG.tile.styleId, css, 'enhanced tile styles'); } /** * Injects global styles (border-radius removal) */ function injectGlobalStyles() { const css = `*{border-radius: 0 !important;}`; injectStyleElement(CONFIG.global.styleId, css, 'global styles'); } // ============================================================================ // PROCESSING FUNCTIONS // ============================================================================ /** * Processes table rows for highlighting updated mods */ function processTable() { if (!isDownloadHistoryPage()) return; const rows = document.querySelectorAll('tr.even, tr.odd'); let highlighted = 0; rows.forEach(row => { const downloadCell = row.querySelector('td.table-download'); const updateCell = row.querySelector('td.table-update'); if (!downloadCell || !updateCell) return; const downloadDate = parseDate(downloadCell.textContent); const updateDate = parseDate(updateCell.textContent); if (!isNaN(downloadDate) && !isNaN(updateDate) && downloadDate < updateDate) { row.classList.add(CONFIG.table.highlightClass); highlighted++; } }); logger.debug(`Processed ${rows.length} table rows, highlighted ${highlighted}`); } /** * Processes mod tiles for highlighting based on badges */ function processTiles() { if (isDownloadHistoryPage()) return; const tileSelector = getTileSelector(); const tiles = document.querySelectorAll(tileSelector); // Clear existing highlights tiles.forEach(tile => { tile.classList.remove(CONFIG.tile.updateClass, CONFIG.tile.downloadClass); }); // Apply highlights based on badges document.querySelectorAll('[data-e2eid="mod-tile-update-available"]').forEach(badge => { const tile = badge.closest(tileSelector); if (tile) tile.classList.add(CONFIG.tile.updateClass); }); document.querySelectorAll('[data-e2eid="mod-tile-downloaded"]').forEach(badge => { const tile = badge.closest(tileSelector); if (tile && !tile.classList.contains(CONFIG.tile.updateClass)) { tile.classList.add(CONFIG.tile.downloadClass); } }); logger.debug(`Processed ${tiles.length} tiles`); } /** * Processes both table and tiles based on current page */ function processAll() { processTable(); processTiles(); } /** * Debounced version of processAll to prevent excessive processing */ const debouncedProcess = debounce(processAll, CONFIG.debounceDelay); // ============================================================================ // OBSERVER & EVENT SETUP // ============================================================================ /** * Sets up MutationObserver for dynamic content changes */ function setupMutationObserver() { if (mutationObserver) mutationObserver.disconnect(); mutationObserver = new MutationObserver(debouncedProcess); mutationObserver.observe(document.body, { childList: true, subtree: true }); } /** * Sets up navigation event listeners for SPA support */ function setupNavigationHooks() { const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { const result = originalPushState.apply(this, args); debouncedProcess(); return result; }; history.replaceState = function(...args) { const result = originalReplaceState.apply(this, args); debouncedProcess(); return result; }; window.addEventListener('popstate', debouncedProcess); } // ============================================================================ // INITIALIZATION // ============================================================================ /** * Main initialization function */ function init() { injectStyles(); processAll(); setupMutationObserver(); setupNavigationHooks(); } // ============================================================================ // SCRIPT STARTUP // ============================================================================ // Start the script when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址