您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds some QoL shortcuts for train navigation on SG!
// ==UserScript== // @name SG Train Navigation Assistant // @namespace http://tampermonkey.net/ // @version 2025-09-27 // @description Adds some QoL shortcuts for train navigation on SG! // @author Alpha2749 | SG /user/Alpha2749 // @match https://www.steamgifts.com/giveaway/* // @icon https://www.google.com/s2/favicons?sz=64&domain=steamgifts.com // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-start // ==/UserScript== (function () { 'use strict'; /* Fallback default config (DO NOT TOUCH) - If you want to modify your configuration please click on your userscript manager and click 'Configure Script' */ const defaultConfig = { allowOpenScreenshots: true, keyBindings: { next: "ArrowRight", previous: "ArrowLeft", screenshots: "ArrowUp", trailerToggle: "Control", } }; let config = loadConfig(); GM_registerMenuCommand("Configure Script", () => { openConfigUI(); }); const nextKeywords = ['next', 'forward', 'on', '>', 'cho', '→', 'N E X T', 'ahead', 'future', 'climbing', '↬', 'avanti', 'prossimo', '▶', 'nekst', 'yes', 'go', '➡️', '⏩', '⏭️', '🌜', '👉', 'Forth']; const lastKeywords = ['prev', 'back', 'last', '<', 'och', '←', 'B A C K', 'retreat', 'past', 'falling', '↫', 'indietro', 'precedente', '◀', 'previous', 'perv', 'prior', 'no', 'og', '⬅️', '⏪', '⏮️', '🌛', '👈']; document.addEventListener("keydown", function (event) { const isInputField = ["INPUT", "TEXTAREA"].includes(document.activeElement.tagName); const configOpen = document.querySelector("#tm-config-ui"); if (isInputField || configOpen) return; const screenshotsOpen = !document.querySelector(".lightbox.hide"); if (screenshotsOpen) { handleScreenshots(event); return; } if (event.key === config.keyBindings.next) handleNavigation("next"); if (event.key === config.keyBindings.previous) handleNavigation("previous"); if (config.allowOpenScreenshots && event.key === config.keyBindings.screenshots) { openScreenshots(); } }); async function handleNavigation(direction) { const link = extractLinks(direction) || findLabelledLink(direction) || findLink(direction); if (link) { showPopup(`Moving ${direction === 'next' ? 'Onward' : 'Backward'}!`); window.location.href = link; } else { showPopup(`Unable to find ${direction} cart. Are you sure you're in a train?`); } } function findLink(direction) { var regex = new RegExp((direction === 'next' ? nextKeywords : lastKeywords).join('|'), 'i'); return Array.from(document.querySelector('.page__description')?.querySelectorAll('a') || []).find(link => { const text = link.textContent.trim(); const url = link.href; const isValidURL = url.includes('/giveaway/') && !url.includes('/discussion/') && !url.includes('/user/'); return isValidURL && regex.test(text); })?.href; } function findLabelledLink(direction) { var regex = new RegExp(`^\\s*(?:${(direction === 'next' ? nextKeywords : lastKeywords).join('|')})(?=\\s*:?)`, 'i'); const container = document.querySelector('.page__description'); if (!container) return null; const lines = container.innerText.split('\n'); for (const line of lines) { if (regex.test(line)) { const match = line.match(/(https?:\/\/[^ \n]+)/); if (match) { const url = match[1]; const isValidURL = url.includes('/giveaway/') && !url.includes('/discussion/') && !url.includes('/user/'); if (isValidURL) return url; } } } return null; } function extractLinks(direction) { const paragraphs = document.querySelector('.page__description')?.querySelectorAll('p, h1, h2') || []; const numbers = Array.from(paragraphs) .flatMap(paragraph => [...paragraph.innerText.matchAll(/\d+/g)].map(match => parseInt(match))) .filter(Boolean); const uniqueNumbers = Array.from(new Set(numbers)).sort((a, b) => a - b); if (uniqueNumbers.length === 0) return null; let run = null; for (let i = 0; i < uniqueNumbers.length; i++) { if (i + 1 < uniqueNumbers.length && uniqueNumbers[i + 1] - uniqueNumbers[i] === 2) { run = [uniqueNumbers[i], uniqueNumbers[i + 1]]; break; } if (i + 2 < uniqueNumbers.length && uniqueNumbers[i + 1] - uniqueNumbers[i] === 1 && uniqueNumbers[i + 2] - uniqueNumbers[i + 1] === 1) { run = [uniqueNumbers[i], uniqueNumbers[i + 1], uniqueNumbers[i + 2]]; break; } } if (!run) return null; const targetNum = direction === 'previous' ? run[0] : run[run.length - 1]; const link = Array.from(document.querySelectorAll('a')).find( a => a.textContent.trim() === targetNum.toString() ); return link ? link.href : null; } function handleScreenshots(event) { if (event.key === config.keyBindings.screenshots) { const closeBtn = document.querySelector('.lightbox-header-icon--close'); closeBtn?.click(); return; } if (event.key === config.keyBindings.trailerToggle) { const imageBtn = document.querySelector('.lightbox-header-icon.fa-camera'); const videoBtn = document.querySelector('.lightbox-header-icon.fa-video-camera'); if (!imageBtn || !videoBtn) return; const isImageSelected = imageBtn.classList.contains('lightbox-header-icon--selected'); if (isImageSelected) { videoBtn.click(); } else { imageBtn.click(); } } } function openScreenshots() { const screenshotBtn = Array.from(document.querySelectorAll('a[data-ui-tooltip]')).find(el => { const tooltipData = el.getAttribute('data-ui-tooltip'); return tooltipData && JSON.parse(tooltipData).rows.some(row => row.columns.some(column => column.name === 'Screenshots / Videos') ); }); screenshotBtn?.click(); } function showPopup(message) { const popup = document.createElement('div'); popup.style.cssText = ` position: fixed; bottom: 20px; right: 20px; padding: 10px; background-color: rgba(0, 0, 0, 0.8); color: white; border-radius: 5px; z-index: 999999; opacity: 0; transform: translateY(20px); transition: opacity 0.2s, transform 0.3s; `; popup.textContent = message; document.body.appendChild(popup); requestAnimationFrame(() => { popup.style.opacity = '1'; popup.style.transform = 'translateY(0)'; }); setTimeout(() => { popup.style.opacity = '0'; popup.style.transform = 'translateY(20px)'; setTimeout(() => { document.body.removeChild(popup); }, 300); }, 2000); } // Config stuff function loadConfig() { const saved = GM_getValue("config", {}); return { ...defaultConfig, ...saved, keyBindings: { ...defaultConfig.keyBindings, ...(saved.keyBindings || {}) } }; } function saveConfig(cfg) { GM_setValue("config", cfg); } let currentClosePopupHandler = null; function openConfigUI() { const existing = document.querySelector("#tm-config-ui"); if (existing) { existing.remove(); return; } const panel = document.createElement("div"); panel.id = "tm-config-ui"; panel.style.position = "fixed"; panel.style.top = "40px"; panel.style.right = "40px"; panel.style.zIndex = "999999"; panel.style.background = "#fff"; panel.style.color = "#333"; panel.style.padding = "20px"; panel.style.border = "1px solid #ccc"; panel.style.borderRadius = "8px"; panel.style.fontFamily = "Segoe UI, sans-serif"; panel.style.minWidth = "280px"; panel.style.boxShadow = "0 4px 16px rgba(0,0,0,0.2)"; panel.innerHTML = ` <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;"> <h3 style="margin:0;font-size:16px;color:#444;">TrainNavAssist Config</h3> <span id="cfg-close" style="cursor:pointer;font-size:16px;color:#999;">✕</span> </div> <label style="display:block;margin-bottom:10px;"> <strong>Next Key:</strong><br> <input type="text" id="cfg-next" value="${config.keyBindings.next}" readonly style="width:100%;padding:6px 8px;margin-top:4px;border:1px solid #ccc;border-radius:4px;"> </label> <label style="display:block;margin-bottom:10px;"> <strong>Previous Key:</strong><br> <input type="text" id="cfg-prev" value="${config.keyBindings.previous}" readonly style="width:100%;padding:6px 8px;margin-top:4px;border:1px solid #ccc;border-radius:4px;"> </label> <label style="display: block; margin-bottom: 16px;"> <strong>Media Keys:</strong><br> Open/ Close Screenshots:<br> <label style="display: flex; width: 100%;"> <input type="text" id="cfg-scr" value="${config.keyBindings.screenshots}" readonly style="padding: 6px 8px; margin-top: 4px; border: 1px solid #ccc; border-radius: 4px; margin-right: 8px;"> <input type="checkbox" id="cfg-screenshots" style="width: 48px;" ${config.allowOpenScreenshots ? "checked" : ""}> </label> Toggle Images/Videos:<br> <input type="text" id="cfg-tra" value="${config.keyBindings.trailerToggle}" readonly style="padding: 6px 8px; margin-top: 4px; border: 1px solid #ccc; border-radius: 4px; margin-right: 8px;"> </label> <div style="display:flex;gap:8px;justify-content:flex-end;"> <button id="cfg-reset" style=" background:#eee;border:1px solid #ccc;border-radius:4px; padding:6px 12px;cursor:pointer;font-size:13px; ">Reset to Default</button> </div> `; document.body.appendChild(panel); panel.querySelector("#cfg-screenshots").addEventListener("change", (e) => { config.allowOpenScreenshots = e.target.checked; showPopup("Screenshot Hotkey " + (config.allowOpenScreenshots ? 'ON' : 'OFF')); saveConfig(config); }); function bindKeyCapture(input, action) { input.addEventListener("focus", () => { input.value = "Press a key..."; }); input.addEventListener("keydown", (e) => { e.preventDefault(); if (e.key === "Escape") { input.value = config.keyBindings[action]; input.blur(); return; } input.value = e.key; config.keyBindings[action] = e.key; saveConfig(config); showPopup("Config Saved"); input.blur(); }); } bindKeyCapture(panel.querySelector("#cfg-next"), "next"); bindKeyCapture(panel.querySelector("#cfg-prev"), "previous"); bindKeyCapture(panel.querySelector("#cfg-scr"), "screenshots"); bindKeyCapture(panel.querySelector("#cfg-tra"), "trailerToggle"); panel.querySelector("#cfg-close").onclick = closePopup; panel.querySelector("#cfg-reset").onclick = () => { config = { ...defaultConfig }; saveConfig(config); showPopup("Config reset to defaults"); closePopup(); }; setTimeout(() => { document.addEventListener("click", closePopupHandler); }, 0); function closePopupHandler(event) { if (!panel.contains(event.target)) { closePopup(); } } function closePopup() { document.removeEventListener("click", closePopupHandler); panel.remove(); return; } } })();