Torn Travel Manual Note

Stable draggable & toggleable travel notes overlay to maximize travel experience. Fully Torn-compliant.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Torn Travel Manual Note
// @namespace    torn.travel.notes.manual
// @version      1.5.0
// @description  Stable draggable & toggleable travel notes overlay to maximize travel experience. Fully Torn-compliant.
// @author       Ms_Mwywnn
// @match        https://www.torn.com/page.php*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    if (!window.location.search.includes("sid=travel")) return;

    const NOTE_KEY = "torn_travel_manual_notes";
    const POS_KEY = "torn_travel_note_position";
    const STATE_KEY = "torn_travel_note_open";

    function waitForTravelPage() {
        const check = setInterval(() => {
            if (document.body.innerText.includes("Traveling")) {
                clearInterval(check);
                init();
            }
        }, 500);
    }

    function getDestination() {
        const match = document.body.innerText.match(/Traveling to\s+([A-Za-z\s]+)/i);
        return match ? match[1].trim() : "Any";
    }

    function init() {
        const destination = getDestination();
        const allNotes = JSON.parse(localStorage.getItem(NOTE_KEY) || "{}");
        const savedPos = JSON.parse(localStorage.getItem(POS_KEY) || "{}");
        const isOpen = localStorage.getItem(STATE_KEY) !== "false";

        /* === Toggle Tab === */
        const tab = document.createElement("div");
        tab.textContent = "📝";
        Object.assign(tab.style, {
            position: "fixed",
            right: "0",
            top: "50%",
            transform: "translateY(-50%)",
            background: "#0b0b0b",
            color: "#ffffff",
            padding: "8px 10px",
            borderRadius: "8px 0 0 8px",
            cursor: "pointer",
            zIndex: "9999",
            fontSize: "14px",
            border: "1px solid #1e90ff"
        });

        /* === Panel === */
        const panel = document.createElement("div");
        Object.assign(panel.style, {
            position: "fixed",
            left: savedPos.left || "calc(100% - 280px)",
            top: savedPos.top || "120px",
            width: "260px",
            background: "#000000",
            color: "#ffffff",
            border: "1px solid #1e90ff",
            borderRadius: "10px",
            padding: "10px",
            zIndex: "9999",
            fontFamily: "Arial, sans-serif",
            boxShadow: "0 0 14px rgba(0,0,0,0.8)",
            display: isOpen ? "block" : "none",
            touchAction: "none"
        });

        panel.innerHTML = `
            <div style="font-weight:bold; margin-bottom:6px; cursor:grab; color:#ffffff;">
                ✈️ Travel Notes
            </div>
            <div style="font-size:12px; color:#7fbfff; margin-bottom:6px;">
                Destination: <span style="color:#ffffff">${destination}</span>
            </div>
            <textarea id="travelNoteBox"
                placeholder="Items to buy, next destination, reminders..."
                style="
                    width:100%;
                    height:120px;
                    background:#0b0b0b;
                    color:#ffffff;
                    border:1px solid #333;
                    border-radius:6px;
                    padding:6px;
                    resize:none;
                    font-size:12px;
                "
            ></textarea>
            <div style="display:flex; justify-content:space-between; margin-top:8px;">
                <button id="clearTravelNote"
                    style="
                        background:#f1c40f;
                        color:#000;
                        border:none;
                        padding:4px 10px;
                        border-radius:6px;
                        font-size:12px;
                        cursor:pointer;
                    "
                >Clear</button>
                <button id="saveTravelNote"
                    style="
                        background:#f1c40f;
                        color:#000;
                        border:none;
                        padding:4px 10px;
                        border-radius:6px;
                        font-size:12px;
                        cursor:pointer;
                    "
                >Save</button>
            </div>
        `;

        document.body.appendChild(tab);
        document.body.appendChild(panel);

        /* === Notes Logic === */
        const textarea = panel.querySelector("#travelNoteBox");
        textarea.value = allNotes[destination] || "";

        panel.querySelector("#saveTravelNote").addEventListener("click", () => {
            allNotes[destination] = textarea.value;
            localStorage.setItem(NOTE_KEY, JSON.stringify(allNotes));
        });

        panel.querySelector("#clearTravelNote").addEventListener("click", () => {
            textarea.value = "";
            delete allNotes[destination];
            localStorage.setItem(NOTE_KEY, JSON.stringify(allNotes));
        });

        /* === Toggle === */
        tab.addEventListener("click", () => {
            const open = panel.style.display === "none";
            panel.style.display = open ? "block" : "none";
            localStorage.setItem(STATE_KEY, open);
        });

        /* === Drag Logic (Stable + Bounded) === */
        let dragging = false;
        let offsetX = 0, offsetY = 0;
        let raf = null;

        function clamp(val, min, max) {
            return Math.max(min, Math.min(max, val));
        }

        function startDrag(x, y) {
            dragging = true;
            document.body.style.overflow = "hidden";
            offsetX = x - panel.offsetLeft;
            offsetY = y - panel.offsetTop;
        }

        function moveDrag(x, y) {
            if (!dragging) return;
            if (raf) cancelAnimationFrame(raf);

            raf = requestAnimationFrame(() => {
                const maxX = window.innerWidth - panel.offsetWidth;
                const maxY = window.innerHeight - panel.offsetHeight;

                panel.style.left = `${clamp(x - offsetX, 0, maxX)}px`;
                panel.style.top = `${clamp(y - offsetY, 0, maxY)}px`;
            });
        }

        function endDrag() {
            if (!dragging) return;
            dragging = false;
            document.body.style.overflow = "";
            localStorage.setItem(POS_KEY, JSON.stringify({
                left: panel.style.left,
                top: panel.style.top
            }));
        }

        function isInteractive(target) {
            return target.tagName === "TEXTAREA" || target.tagName === "BUTTON";
        }

        panel.addEventListener("mousedown", e => {
            if (isInteractive(e.target)) return;
            startDrag(e.clientX, e.clientY);
        });

        document.addEventListener("mousemove", e => moveDrag(e.clientX, e.clientY));
        document.addEventListener("mouseup", endDrag);

        panel.addEventListener("touchstart", e => {
            if (isInteractive(e.target)) return;
            e.preventDefault();
            const t = e.touches[0];
            startDrag(t.clientX, t.clientY);
        }, { passive: false });

        document.addEventListener("touchmove", e => {
            if (!dragging) return;
            e.preventDefault();
            const t = e.touches[0];
            moveDrag(t.clientX, t.clientY);
        }, { passive: false });

        document.addEventListener("touchend", endDrag);
    }

    waitForTravelPage();
})();