SG Train Navigation Assistant

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