Torn Player Notepad

Persistent player notepad with Import/Export. Multiple notes per player.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Torn Player Notepad
// @namespace    https://torn.com
// @version      1.1
// @description  Persistent player notepad with Import/Export. Multiple notes per player.
// @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));
    }

    // ====================== IMPORT / EXPORT ======================
    function exportAllNotes() {
        const backup = {};
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key && key.startsWith(STORAGE_PREFIX)) {
                const playerId = key.replace(STORAGE_PREFIX, '');
                backup[playerId] = JSON.parse(localStorage.getItem(key));
            }
        }

        const dataStr = JSON.stringify(backup, null, 2);
        const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
        const exportFileDefaultName = `torn-player-notes-backup-${new Date().toISOString().slice(0,10)}.json`;

        const linkElement = document.createElement('a');
        linkElement.setAttribute('href', dataUri);
        linkElement.setAttribute('download', exportFileDefaultName);
        linkElement.click();
    }

    function importNotes(file) {
        if (!file) return;
        const reader = new FileReader();
        reader.onload = function(e) {
            try {
                const imported = JSON.parse(e.target.result);
                let importedCount = 0;

                Object.keys(imported).forEach(playerId => {
                    if (!playerId || !Array.isArray(imported[playerId])) return;

                    const existing = loadNotes(playerId);
                    const merged = [...existing];

                    imported[playerId].forEach(newNote => {
                        if (!merged.some(n => n.id === newNote.id)) {
                            merged.push(newNote);
                            importedCount++;
                        }
                    });

                    saveNotes(playerId, merged);
                });

                alert(`✅ Successfully imported ${importedCount} note(s)!`);

                if (modalOverlay && modalOverlay.style.display === 'flex') {
                    currentNotes = loadNotes(currentPlayerId);
                    renderNotesList(modalOverlay);
                    if (currentNoteId) loadNote(currentNoteId, modalOverlay);
                }
            } catch (err) {
                alert('❌ Invalid backup file.');
            }
        };
        reader.readAsText(file);
    }

    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"/>
            <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"/>
            <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;

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

        const newButton = createNotepadButton(currentPlayerId);
        buttonsList.appendChild(newButton);
    }

    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; flex-wrap: wrap; gap: 8px; }
            .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.dataset.id = note.id;
            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>
            `;

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

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

    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;

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

                saveNotes(currentPlayerId, currentNotes);

                const listItem = overlay.querySelector(`.torn-notepad-list-item[data-id="${currentNoteId}"]`);
                if (listItem) {
                    const titleDiv = listItem.querySelector('div[style*="font-weight"]');
                    if (titleDiv) titleDiv.textContent = newTitle;
                }
            }, 600);
        };

        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..."></textarea>
                        </div>
                    </div>
                    <div class="torn-notepad-footer">
                        <div>
                            <button id="new-note-btn" class="torn-notepad-new-btn">+ New Note</button>
                            <button id="export-btn" style="background:#2e7d32;color:white;margin-left:8px;padding:9px 16px;border:none;border-radius:4px;cursor:pointer;">Export All Notes</button>
                            <label style="margin-left:8px;">
                                <button id="import-btn" style="background:#f57c00;color:white;padding:9px 16px;border:none;border-radius:4px;cursor:pointer;">Import</button>
                                <input type="file" id="import-file" accept=".json" style="display:none;">
                            </label>
                        </div>
                        <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);

            // Event Listeners
            modalOverlay.querySelector('#modal-close').onclick =
            modalOverlay.querySelector('#modal-close2').onclick = () => modalOverlay.style.display = 'none';

            modalOverlay.querySelector('#new-note-btn').onclick = () => createNewNote(modalOverlay);
            modalOverlay.querySelector('#export-btn').onclick = exportAllNotes;

            const importInput = modalOverlay.querySelector('#import-file');
            modalOverlay.querySelector('#import-btn').onclick = () => importInput.click();
            importInput.onchange = (e) => {
                if (e.target.files[0]) importNotes(e.target.files[0]);
                e.target.value = '';
            };

            modalOverlay.onclick = (e) => { if (e.target === modalOverlay) modalOverlay.style.display = 'none'; };

            setupAutoSave(modalOverlay);
        }

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

        currentNoteId = currentNotes.length ? currentNotes[0].id : null;
        renderNotesList(modalOverlay);
        if (currentNoteId) loadNote(currentNoteId, modalOverlay);

        modalOverlay.style.display = 'flex';
    }

    function init() {
        const observer = new MutationObserver(() => {
            if (document.querySelector('.buttons-list')) insertNotepadButton();
        });

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

        setTimeout(insertNotepadButton, 800);
        setTimeout(insertNotepadButton, 2500);

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

    window.addEventListener('load', init);
})();