// ==UserScript==
// @name Nexus Mods 自动慢速下载
// @namespace https://github.com/yg/nexus-auto-slow
// @version 1.2.1
// @description 按完两次 Manual download 后,自动点击 Slow download,省去多点一步
// @author YG
// @match *://*.nexusmods.com/*
// @match *://nexusmods.com/*
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
var TAG = "[Nexus Auto Slow]";
var CLICK_COOLDOWN = 2000;
var lastClick = 0;
var clickedEls = new WeakSet();
var clickedFlag = false;
var injected = false;
var pollTimer = null;
function norm(t) { return (t || "").replace(/\s+/g, " ").trim().toLowerCase(); }
function isVisible(el) {
if (!el || !el.getBoundingClientRect) return false;
var r = el.getBoundingClientRect();
if (r.width <= 0 && r.height <= 0) return false;
var s = el.ownerDocument.defaultView.getComputedStyle(el);
return s.visibility !== "hidden" && s.display !== "none" && s.opacity !== "0";
}
function isSlowText(el) {
var t = norm(el.textContent);
return t === "slow download" || (t.indexOf("slow download") !== -1 && t.length <= 25);
}
function collectDocs() {
var docs = [document];
var frames = document.querySelectorAll("iframe");
for (var i = 0; i < frames.length; i++) {
try { if (frames[i].contentDocument) docs.push(frames[i].contentDocument); } catch (e) {}
}
return docs;
}
function allRoots() {
var roots = [];
function walk(root) {
roots.push(root);
var els = root.querySelectorAll("*");
for (var i = 0; i < els.length; i++) { if (els[i].shadowRoot) walk(els[i].shadowRoot); }
}
var docs = collectDocs();
for (var j = 0; j < docs.length; j++) walk(docs[j]);
return roots;
}
function findSlow() {
var roots = allRoots();
for (var i = 0; i < roots.length; i++) {
var btns = roots[i].querySelectorAll("button, a, [role='button']");
for (var b = 0; b < btns.length; b++) { if (isSlowText(btns[b]) && isVisible(btns[b])) return btns[b]; }
var spans = roots[i].querySelectorAll("span");
for (var s = 0; s < spans.length; s++) {
if (norm(spans[s].textContent) === "slow download") {
var c = spans[s].closest("button, a, [role='button']");
if (c && isVisible(c)) return c;
}
}
}
return null;
}
function fireClick(el) {
var opts = { bubbles: true, cancelable: true, composed: true, view: el.ownerDocument.defaultView, button: 0 };
try { el.dispatchEvent(new PointerEvent("pointerdown", opts)); } catch (e) {}
try { el.dispatchEvent(new MouseEvent("mousedown", opts)); } catch (e) {}
try { el.dispatchEvent(new PointerEvent("pointerup", opts)); } catch (e) {}
try { el.dispatchEvent(new MouseEvent("mouseup", opts)); } catch (e) {}
try { el.dispatchEvent(new MouseEvent("click", opts)); } catch (e) {}
try { el.click(); } catch (e) {}
}
function buildToast() {
var t = document.createElement("div");
t.id = "__nexusSlowToast";
t.style.cssText = "display:flex;align-items:center;gap:8px;" +
"background:#1b1b20;color:#e7e7ea;border:1px solid rgba(218,142,53,.55);border-left:3px solid #da8e35;" +
"border-radius:8px;padding:8px 12px;box-shadow:0 8px 24px rgba(0,0,0,.45);" +
"font:600 13px/1.4 'Segoe UI',Roboto,-apple-system,sans-serif;letter-spacing:.2px;white-space:nowrap;" +
"opacity:0;transform:translateY(-4px);transition:opacity .25s ease,transform .25s ease;";
t.innerHTML = '' +
'' +
'Nexusmods 自动下载已触发';
return t;
}
function showToast() {
if (injected) return;
if (document.getElementById("__nexusSlowToast") || document.querySelectorAll("#__nexusSlowToast").length) { injected = true; return; }
var anchor = null;
var roots = allRoots();
for (var i = 0; i < roots.length && !anchor; i++) {
var els = roots[i].querySelectorAll("div, section, span, strong, h1, h2, h3, p");
for (var e = 0; e < els.length; e++) {
var txt = norm(els[e].textContent);
if (/your download is starting|download didn.t start|start download manually/.test(txt) && txt.length <= 160) {
var node = els[e];
for (var up = 0; up < 5 && node.parentElement; up++) {
var r = node.getBoundingClientRect();
if (r.width > 220 && r.height > 44) break;
node = node.parentElement;
}
anchor = node;
break;
}
}
}
var toast = buildToast();
if (anchor && anchor.appendChild && anchor.ownerDocument) {
var cs = anchor.ownerDocument.defaultView.getComputedStyle(anchor);
if (cs.position === "static") anchor.style.position = "relative";
toast.style.position = "absolute";
toast.style.top = "10px";
toast.style.right = "10px";
toast.style.zIndex = "2147483647";
anchor.appendChild(toast);
} else {
toast.style.position = "fixed";
toast.style.top = "16px";
toast.style.right = "16px";
toast.style.zIndex = "2147483647";
(document.body || document.documentElement).appendChild(toast);
}
injected = true;
requestAnimationFrame(function () { toast.style.opacity = "1"; toast.style.transform = "translateY(0)"; });
setTimeout(function () {
toast.style.opacity = "0";
toast.style.transform = "translateY(-4px)";
setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
}, 3000);
console.info(TAG, "已显示成功提示(3 秒后自动关闭)");
}
function startPollSuccess() {
if (pollTimer) return;
var tries = 0;
pollTimer = setInterval(function () {
tries++;
showToast();
if (injected || tries > 40) { clearInterval(pollTimer); pollTimer = null; }
}, 500);
showToast();
}
function tryClick() {
if (clickedFlag) { return false; }
var btn = findSlow();
if (!btn) { return false; }
if (clickedEls.has(btn)) return false;
var now = Date.now();
if (now - lastClick < CLICK_COOLDOWN) return false;
clickedEls.add(btn);
lastClick = now;
try { btn.scrollIntoView({ block: "center", inline: "center" }); } catch (e) {}
fireClick(btn);
clickedFlag = true;
console.info(TAG, "已触发点击 Slow download");
startPollSuccess();
return true;
}
var pending = null;
function schedule() { if (pending) return; pending = setTimeout(function () { pending = null; tryClick(); }, 200); }
function start() {
console.info(TAG, "脚本已启动,监听 Slow download");
try { window.__nexusSlowDebug = function () { var b = findSlow(); return b ? (b.tagName + "|" + b.textContent) : "none"; }; } catch (e) {}
try { new MutationObserver(schedule).observe(document.documentElement, { childList: true, subtree: true }); } catch (e) {}
setInterval(tryClick, 700);
tryClick();
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
else start();
})();