Torn Player Notepad

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();