Torn Player Notepad

Persistent player notepad for Torn.com profiles. Multiple notes per player with full CRUD. Survives cache clear.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Player Notepad
// @namespace    https://torn.com
// @version      1.0
// @description  Persistent player notepad for Torn.com profiles. Multiple notes per player with full CRUD. Survives cache clear.
// @author       TheStonedVibeCoder + Grok
// @match        https://www.torn.com/profiles.php?XID=*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_PREFIX = 'torn_player_notes_';

    let currentPlayerId = null;
    let currentNotes = [];
    let currentNoteId = null;
    let modalOverlay = null;

    function getPlayerId() {
        const h4 = document.getElementById('skip-to-content');
        if (!h4) return null;
        const match = h4.textContent.trim().match(/\[(\d+)\]/);
        return match ? match[1] : null;
    }

    function loadNotes(playerId) {
        if (!playerId) return [];
        const key = STORAGE_PREFIX + playerId;
        const data = localStorage.getItem(key);
        return data ? JSON.parse(data) : [];
    }

    function saveNotes(playerId, notes) {
        if (!playerId) return;
        const key = STORAGE_PREFIX + playerId;
        localStorage.setItem(key, JSON.stringify(notes));
    }

    function createNotepadSVG() {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("width", "46");
        svg.setAttribute("height", "46");
        svg.setAttribute("viewBox", "0 0 46 46");
        svg.setAttribute("class", "icon___oJODA");

        svg.innerHTML = `
        <rect x="10" y="9" width="26" height="29" rx="3" ry="3" fill="#2a2a2a" stroke="#ccc" stroke-width="3"/>

        <!-- Spiral rings -->
        <circle cx="15" cy="14" r="1.5" fill="#ccc"/>
        <circle cx="15" cy="20" r="1.5" fill="#ccc"/>
        <circle cx="15" cy="26" r="1.5" fill="#ccc"/>
        <circle cx="15" cy="32" r="1.5" fill="#ccc"/>

        <!-- Paper lines -->
        <line x1="21" y1="15" x2="33" y2="15" stroke="#ccc" stroke-width="2"/>
        <line x1="21" y1="20" x2="33" y2="20" stroke="#ccc" stroke-width="2"/>
        <line x1="21" y1="25" x2="33" y2="25" stroke="#ccc" stroke-width="2"/>
        <line x1="21" y1="30" x2="30" y2="30" stroke="#ccc" stroke-width="2"/>
    `;
        return svg;
    }

    function createNotepadButton(playerId) {
        const button = document.createElement('a');
        button.id = `button-notepad-profile-${playerId}`;
        button.href = "#";
        button.className = "profile-button active";
        button.setAttribute("aria-label", "Player Notepad");
        button.setAttribute("style", "touch-action: manipulation;");
        button.style.display = "flex";
        button.style.alignItems = "center";
        button.style.justifyContent = "center";

        const svg = createNotepadSVG();
        button.appendChild(svg);

        button.addEventListener('click', (e) => {
            e.preventDefault();
            openNotepadModal();
        });

        return button;
    }

    function insertNotepadButton() {
        const buttonsList = document.querySelector('.buttons-list');
        if (!buttonsList) return;

        currentPlayerId = getPlayerId();
        if (!currentPlayerId) return;

        // Prevent duplicate buttons
        if (document.getElementById(`button-notepad-profile-${currentPlayerId}`)) return;

        const newButton = createNotepadButton(currentPlayerId);
        buttonsList.appendChild(newButton);
        console.log('✅ Torn Player Notepad button added to actions');
    }

    function createAndInjectStyles() {
        if (document.getElementById('torn-notepad-styles')) return;

        const style = document.createElement('style');
        style.id = 'torn-notepad-styles';
        style.textContent = `
            .torn-notepad-overlay {
                position: fixed !important;
                top: 0;
                left: 0;
                width: 100vw;
                height: 100vh;
                background: rgba(0, 0, 0, 0.88);
                z-index: 2147483647 !important;
                display: none;
                align-items: center;
                justify-content: center;
            }
            .torn-notepad-modal {
                background: #1c1c1c;
                border: 3px solid #444;
                border-radius: 6px;
                width: 95%;
                max-width: 1100px;
                height: 80vh;
                max-height: 700px;
                display: flex;
                flex-direction: column;
                box-shadow: 0 10px 40px rgba(0,0,0,0.9);
                overflow: hidden;
                color: #e0e0e0;
                font-family: Arial, sans-serif;
            }
            .torn-notepad-header {
                padding: 14px 20px;
                background: #282828;
                border-bottom: 1px solid #555;
                display: flex;
                align-items: center;
                justify-content: space-between;
                font-size: 18px;
            }
            .torn-notepad-body {
                display: flex;
                flex: 1;
                overflow: hidden;
            }
            .torn-notepad-list {
                width: 300px;
                background: #242424;
                border-right: 1px solid #4a4a4a;
                overflow-y: auto;
            }
            .torn-notepad-list-item {
                padding: 14px 18px;
                border-bottom: 1px solid #3a3a3a;
                cursor: pointer;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .torn-notepad-list-item:hover { background: #333; }
            .torn-notepad-list-item.active { background: #3b5e8a; color: white; }
            .torn-notepad-editor {
                flex: 1;
                display: flex;
                flex-direction: column;
                padding: 20px;
                gap: 12px;
            }
            .torn-notepad-editor input {
                background: #2f2f2f;
                border: 2px solid #555;
                color: #fff;
                padding: 10px 14px;
                font-size: 17px;
                border-radius: 4px;
            }
            .torn-notepad-editor textarea {
                flex: 1;
                background: #2f2f2f;
                border: 2px solid #555;
                color: #ddd;
                padding: 14px;
                font-size: 15px;
                line-height: 1.5;
                resize: none;
                border-radius: 4px;
                font-family: system-ui;
            }
            .torn-notepad-trash {
                color: #e05c5c;
                font-size: 20px;
                cursor: pointer;
                padding: 0 4px;
            }
            .torn-notepad-footer {
                padding: 12px 20px;
                background: #282828;
                border-top: 1px solid #555;
                display: flex;
                justify-content: space-between;
            }
            .torn-notepad-new-btn {
                background: #4678b5;
                color: white;
                border: none;
                padding: 9px 18px;
                border-radius: 4px;
                cursor: pointer;
                font-weight: bold;
            }
        `;
        document.head.appendChild(style);
    }

    function renderNotesList(overlay) {
        const listEl = overlay.querySelector('#notes-list');
        listEl.innerHTML = '';

        if (currentNotes.length === 0) {
            const empty = document.createElement('div');
            empty.style.padding = '40px 20px';
            empty.style.textAlign = 'center';
            empty.style.color = '#777';
            empty.textContent = 'No notes yet.\nClick "New Note" to get started.';
            listEl.appendChild(empty);
            return;
        }

        currentNotes.forEach(note => {
            const div = document.createElement('div');
            div.className = `torn-notepad-list-item ${note.id === currentNoteId ? 'active' : ''}`;
            div.innerHTML = `
                <div style="flex: 1; min-width: 0;">
                    <div style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${note.title || 'Untitled Note'}</div>
                </div>
                <span class="torn-notepad-trash" data-id="${note.id}" title="Delete note">🗑</span>
            `;

            // Click anywhere except trash to load note
            div.addEventListener('click', (e) => {
                if (e.target.classList.contains('torn-notepad-trash') || e.target.getAttribute('data-id')) return;
                loadNote(note.id, overlay);
            });

            // Trash delete handler
            const trashBtn = div.querySelector('.torn-notepad-trash');
            trashBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                if (confirm('Delete this note permanently?')) {
                    deleteNote(note.id, overlay);
                }
            });

            listEl.appendChild(div);
        });
    }

    function loadNote(noteId, overlay) {
        currentNoteId = noteId;
        const note = currentNotes.find(n => n.id === noteId);
        if (!note) return;

        const titleInput = overlay.querySelector('#note-title');
        const contentArea = overlay.querySelector('#note-content');

        titleInput.value = note.title || '';
        contentArea.value = note.content || '';

        renderNotesList(overlay);
    }

    function createNewNote(overlay) {
        const newNote = {
            id: 'note_' + Date.now(),
            title: 'New Note',
            content: '',
            createdAt: Date.now(),
            updatedAt: Date.now()
        };
        currentNotes.unshift(newNote); // newest on top
        saveNotes(currentPlayerId, currentNotes);
        currentNoteId = newNote.id;
        renderNotesList(overlay);
        loadNote(newNote.id, overlay);
    }

    function deleteNote(noteId, overlay) {
        currentNotes = currentNotes.filter(n => n.id !== noteId);
        if (currentNoteId === noteId) {
            currentNoteId = currentNotes.length ? currentNotes[0].id : null;
        }
        saveNotes(currentPlayerId, currentNotes);
        renderNotesList(overlay);
        if (currentNoteId) {
            loadNote(currentNoteId, overlay);
        } else {
            const titleInput = overlay.querySelector('#note-title');
            const contentArea = overlay.querySelector('#note-content');
            titleInput.value = '';
            contentArea.value = '';
        }
    }

    function setupAutoSave(overlay) {
        const titleInput = overlay.querySelector('#note-title');
        const contentArea = overlay.querySelector('#note-content');

        let timeout;
        const autoSave = () => {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                if (!currentNoteId || !currentPlayerId) return;
                const noteIndex = currentNotes.findIndex(n => n.id === currentNoteId);
                if (noteIndex === -1) return;

                currentNotes[noteIndex].title = titleInput.value.trim() || 'Untitled Note';
                currentNotes[noteIndex].content = contentArea.value;
                currentNotes[noteIndex].updatedAt = Date.now();

                saveNotes(currentPlayerId, currentNotes);
            }, 700);
        };

        titleInput.addEventListener('input', autoSave);
        contentArea.addEventListener('input', autoSave);
    }

    function openNotepadModal() {
        currentPlayerId = getPlayerId();
        if (!currentPlayerId) {
            alert("Could not detect player ID. Try refreshing the page.");
            return;
        }

        currentNotes = loadNotes(currentPlayerId);

        if (!modalOverlay) {
            createAndInjectStyles();
            modalOverlay = document.createElement('div');
            modalOverlay.className = 'torn-notepad-overlay';
            modalOverlay.innerHTML = `
                <div class="torn-notepad-modal">
                    <div class="torn-notepad-header">
                        <div><strong>📝 Player Notepad</strong> — <span id="modal-player-name" style="color:#8ab4f7;"></span></div>
                        <button id="modal-close" style="background:none;border:none;color:#ddd;font-size:28px;line-height:1;cursor:pointer;padding:0 10px;">×</button>
                    </div>
                    <div class="torn-notepad-body">
                        <div class="torn-notepad-list" id="notes-list"></div>
                        <div class="torn-notepad-editor">
                            <input type="text" id="note-title" placeholder="Note title">
                            <textarea id="note-content" placeholder="Write your notes about this player here... (lines, reminders, intel, etc.)"></textarea>
                        </div>
                    </div>
                    <div class="torn-notepad-footer">
                        <button id="new-note-btn" class="torn-notepad-new-btn">+ New Note</button>
                        <button id="modal-close2" style="background:#3a3a3a;color:#ddd;border:none;padding:9px 20px;border-radius:4px;cursor:pointer;">Close</button>
                    </div>
                </div>
            `;
            document.body.appendChild(modalOverlay);

            // Close buttons
            modalOverlay.querySelector('#modal-close').onclick =
            modalOverlay.querySelector('#modal-close2').onclick = () => {
                modalOverlay.style.display = 'none';
            };

            // New note button
            modalOverlay.querySelector('#new-note-btn').onclick = () => createNewNote(modalOverlay);

            // Click outside modal to close
            modalOverlay.onclick = (e) => {
                if (e.target === modalOverlay) modalOverlay.style.display = 'none';
            };

            setupAutoSave(modalOverlay);
        }

        // Update player name in header
        const playerNameEl = modalOverlay.querySelector('#modal-player-name');
        const h4 = document.getElementById('skip-to-content');
        if (playerNameEl && h4) {
            playerNameEl.textContent = h4.textContent.trim();
        }

        // Load initial state
        currentNoteId = currentNotes.length ? currentNotes[0].id : null;
        renderNotesList(modalOverlay);

        if (currentNoteId) {
            loadNote(currentNoteId, modalOverlay);
        } else {
            const titleInput = modalOverlay.querySelector('#note-title');
            const contentArea = modalOverlay.querySelector('#note-content');
            titleInput.value = '';
            contentArea.value = '';
        }

        modalOverlay.style.display = 'flex';
    }

    function init() {
        // Use MutationObserver because Torn loads dynamically
        const observer = new MutationObserver(() => {
            const buttonsList = document.querySelector('.buttons-list');
            if (buttonsList) {
                insertNotepadButton();
            }
        });

        observer.observe(document.documentElement, { childList: true, subtree: true });

        // Fallback for immediate load
        setTimeout(insertNotepadButton, 800);
        setTimeout(insertNotepadButton, 2500);

        console.log('%c📝 Torn Player Notepad script loaded successfully!', 'color: #4ade80; font-weight: bold; font-size: 13px;');
    }

    // Start everything
    window.addEventListener('load', init);
})();