// ==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(); })();