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