Persistent player notepad for Torn.com profiles. Multiple notes per player with full CRUD. Survives cache clear.
// ==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);
})();