// ==UserScript==
// @name Nexus Mods - Updated Mod Highlighter
// @version 2.1.0
// @description Highlight mods that have updated since you last downloaded them
// @author Journey Over
// @license MIT
// @match *://*.nexusmods.com/*
// @require https://cdn.jsdelivr.net/gh/StylusThemes/Userscripts@9db06a14c296ae584e0723cde883729d819e0625/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://greasyfork.org/users/32214
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
table: {
highlightClass: 'nm-update-row',
},
tile: {
styleId: 'nm-highlighter-style',
updateClass: 'nm-update-card',
downloadClass: 'nm-downloaded-card',
colors: {
update: {
primary: 'rgba(255,59,48,0.8)',
secondary: 'rgba(255,100,92,0.6)',
glow: 'rgba(255,59,48,0.4)',
bg: 'rgba(255,59,48,0.05)'
},
download: {
primary: 'rgba(52,199,89,0.8)',
secondary: 'rgba(92,255,129,0.6)',
glow: 'rgba(52,199,89,0.4)',
bg: 'rgba(52,199,89,0.05)'
}
},
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: {
styleId: 'nexus-global-style',
},
debounceDelay: 100,
};
const ANIMATION_DURATIONS = {
TILE_GLOW: 2,
TILE_PULSE: 2.5,
TABLE_GLOW: 3,
TABLE_STRIPE: 4,
};
const PAGE_SELECTORS = {
DOWNLOAD_HISTORY: {
path: '/users/myaccount',
tab: 'tab=download+history'
}
};
class NexusModsHighlighter {
constructor() {
this.logger = Logger('Nexus Mods - Updated Mod Highlighter', { debug: false });
this.mutationObserver = null;
this.debouncedProcess = debounce(this.processAll.bind(this), CONFIG.debounceDelay);
}
parseDate(text) {
if (!text) return NaN;
const cleanedText = text.replace(/\s+/g, ' ').trim();
const parsedTimestamp = Date.parse(cleanedText);
return isNaN(parsedTimestamp) ? new Date(cleanedText).getTime() || NaN : parsedTimestamp;
}
isDownloadHistoryPage() {
return window.location.pathname.includes(PAGE_SELECTORS.DOWNLOAD_HISTORY.path) &&
window.location.search.includes(PAGE_SELECTORS.DOWNLOAD_HISTORY.tab);
}
getTileSelector() {
return CONFIG.tile.selectors.join(', ');
}
injectStyleElement(styleId, styleCss) {
if (document.getElementById(styleId)) return;
const styleElement = document.createElement('style');
styleElement.id = styleId;
styleElement.textContent = styleCss;
document.head.appendChild(styleElement);
this.logger.debug(`Injected style: ${styleId}`);
}
injectTableStyles() {
const updateColors = CONFIG.tile.colors.update;
const css = `@keyframes table-row-glow{0%,100%{box-shadow:inset 0 0 8px ${updateColors.glow.replace('0.4','0.1')},0 0 4px ${updateColors.glow.replace('0.4','0.2')};background:linear-gradient(90deg,${updateColors.bg} 0%,${updateColors.bg.replace('0.05','0.08')} 50%,${updateColors.bg} 100%)}50%{box-shadow:inset 0 0 12px ${updateColors.glow.replace('0.4','0.15')},0 0 8px ${updateColors.glow.replace('0.4','0.3')};background:linear-gradient(90deg,${updateColors.bg.replace('0.05','0.08')} 0%,${updateColors.bg.replace('0.05','0.12')} 50%,${updateColors.bg.replace('0.05','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,${updateColors.bg.replace('0.05','0.03')} 0%,${updateColors.bg.replace('0.05','0.06')} 50%,${updateColors.bg.replace('0.05','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%,${updateColors.glow.replace('0.4','0.1')} 20%,${updateColors.glow.replace('0.4','0.2')} 50%,${updateColors.glow.replace('0.4','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,${updateColors.primary} 0%,${updateColors.secondary} 50%,${updateColors.primary} 100%);box-shadow:0 0 8px ${updateColors.glow}}`;
this.injectStyleElement('nexus-updated-style', css);
}
injectTileStyles() {
const updateColors = CONFIG.tile.colors.update;
const downloadColors = CONFIG.tile.colors.download;
const css = `@keyframes nm-glow{0%,100%{box-shadow:0 0 8px ${updateColors.glow},0 0 16px ${updateColors.glow.replace('0.4','0.2')},0 0 24px ${updateColors.glow.replace('0.4','0.1')},inset 0 0 8px ${updateColors.glow.replace('0.4','0.1')};filter:brightness(1.05) saturate(1.1)}50%{box-shadow:0 0 12px ${updateColors.primary.replace('0.8','0.6')},0 0 24px ${updateColors.primary.replace('0.8','0.4')},0 0 36px ${updateColors.primary.replace('0.8','0.2')},inset 0 0 12px ${updateColors.primary.replace('0.8','0.15')};filter:brightness(1.08) saturate(1.15)}}@keyframes nm-download-pulse{0%,100%{box-shadow:0 0 6px ${downloadColors.glow},0 0 12px ${downloadColors.glow.replace('0.4','0.2')},inset 0 0 6px ${downloadColors.glow.replace('0.4','0.05')}}50%{box-shadow:0 0 10px ${downloadColors.primary.replace('0.8','0.5')},0 0 20px ${downloadColors.primary.replace('0.8','0.3')},inset 0 0 10px ${downloadColors.primary.replace('0.8','0.08')}}} .${CONFIG.tile.updateClass}{position:relative;background:linear-gradient(135deg,${updateColors.bg} 0%,${updateColors.bg.replace('0.05','0.03')} 50%,${updateColors.bg.replace('0.05','0.01')} 100%);border:2px solid transparent;border-image:linear-gradient(135deg,${updateColors.primary} 0%,${updateColors.secondary} 50%,${updateColors.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%,${updateColors.bg.replace('0.05','0.1')} 25%,${updateColors.bg.replace('0.05','0.2')} 50%,${updateColors.bg.replace('0.05','0.1')} 75%,transparent 100%);background-size:200% 200%;animation:nm-glow ${ANIMATION_DURATIONS.TILE_GLOW}s ease-in-out infinite;pointer-events:none;z-index:-1}.${CONFIG.tile.downloadClass}{position:relative;background:linear-gradient(135deg,${downloadColors.bg} 0%,${downloadColors.bg.replace('0.05','0.03')} 50%,${downloadColors.bg.replace('0.05','0.01')} 100%);border:2px solid transparent;border-image:linear-gradient(135deg,${downloadColors.primary} 0%,${downloadColors.secondary} 50%,${downloadColors.primary.replace('0.8','0.4')} 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}::before{content:'';position:absolute;top:-2px;left:-2px;right:-2px;bottom:-2px;background:linear-gradient(45deg,transparent 0%,${downloadColors.bg.replace('0.05','0.1')} 25%,${downloadColors.bg.replace('0.05','0.2')} 50%,${downloadColors.bg.replace('0.05','0.1')} 75%,transparent 100%);background-size:200% 200%;animation:nm-download-pulse ${ANIMATION_DURATIONS.TILE_PULSE}s ease-in-out infinite;pointer-events:none;z-index:-1}`;
this.injectStyleElement(CONFIG.tile.styleId, css);
}
injectGlobalStyles() {
const css = `*{border-radius:0!important}`;
this.injectStyleElement(CONFIG.global.styleId, css);
}
injectStyles() {
this.injectTableStyles();
this.injectTileStyles();
this.injectGlobalStyles();
}
processTable() {
if (!this.isDownloadHistoryPage()) return;
const tableRows = document.querySelectorAll('tr.even, tr.odd');
let highlightedCount = 0;
for (const tableRow of tableRows) {
const downloadDateCell = tableRow.querySelector('td.table-download');
const updateDateCell = tableRow.querySelector('td.table-update');
if (!downloadDateCell || !updateDateCell) continue;
const downloadTimestamp = this.parseDate(downloadDateCell.textContent);
const updateTimestamp = this.parseDate(updateDateCell.textContent);
if (!isNaN(downloadTimestamp) && !isNaN(updateTimestamp) && downloadTimestamp < updateTimestamp) {
tableRow.classList.add(CONFIG.table.highlightClass);
highlightedCount++;
}
}
this.logger.debug(`Processed ${tableRows.length} table rows, highlighted ${highlightedCount}`);
}
processTiles() {
if (this.isDownloadHistoryPage()) return;
const tileSelectorString = this.getTileSelector();
const tileElements = document.querySelectorAll(tileSelectorString);
for (const tileElement of tileElements) {
tileElement.classList.remove(CONFIG.tile.updateClass, CONFIG.tile.downloadClass);
}
for (const updateBadge of document.querySelectorAll('[data-e2eid="mod-tile-update-available"]')) {
const tileElement = updateBadge.closest(tileSelectorString);
if (tileElement) tileElement.classList.add(CONFIG.tile.updateClass);
}
for (const downloadBadge of document.querySelectorAll('[data-e2eid="mod-tile-downloaded"]')) {
const tileElement = downloadBadge.closest(tileSelectorString);
if (tileElement && !tileElement.classList.contains(CONFIG.tile.updateClass)) {
tileElement.classList.add(CONFIG.tile.downloadClass);
}
}
this.logger.debug(`Processed ${tileElements.length} tiles`);
}
processAll() {
this.processTable();
this.processTiles();
}
setupMutationObserver() {
if (this.mutationObserver) this.mutationObserver.disconnect();
this.mutationObserver = new MutationObserver(this.debouncedProcess);
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
setupNavigationHooks() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (...stateArguments) => {
const result = originalPushState.apply(history, stateArguments);
this.debouncedProcess();
return result;
};
history.replaceState = (...stateArguments) => {
const result = originalReplaceState.apply(history, stateArguments);
this.debouncedProcess();
return result;
};
window.addEventListener('popstate', this.debouncedProcess);
}
init() {
this.injectStyles();
this.processAll();
this.setupMutationObserver();
this.setupNavigationHooks();
}
}
const highlighter = new NexusModsHighlighter();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => highlighter.init());
} else {
highlighter.init();
}
})();