Persistent player notepad with Import/Export. Multiple notes per player.
// ==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);
})();