您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A notepad so fancy it should be a paid SaaS
// ==UserScript== // @name Notepad For Character.ai // @namespace http://tampermonkey.net/ // @version 9.3.0 // @description A notepad so fancy it should be a paid SaaS // @author Mr005K via ChatGPT // @match https://character.ai/* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ------------------------ Runtime Loader ------------------------ function loadScript(url) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = url; s.async = true; s.onload = resolve; s.onerror = () => reject(new Error(`Failed: ${url}`)); document.head.appendChild(s); }); } async function loadLibraries() { const libs = [ { name: 'marked', check: () => window.marked, urls: [ 'https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js', 'https://unpkg.com/[email protected]/marked.min.js' ] }, { name: 'DOMPurify', check: () => window.DOMPurify, urls: [ 'https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.2/purify.min.js', 'https://unpkg.com/[email protected]/dist/purify.min.js' ] }, { name: 'LZString', check: () => window.LZString, urls: [ 'https://cdn.jsdelivr.net/npm/[email protected]/libs/lz-string.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js', 'https://unpkg.com/[email protected]/libs/lz-string.min.js' ] }, { name: 'twemoji', check: () => window.twemoji, urls: [ 'https://cdn.jsdelivr.net/npm/[email protected]/dist/twemoji.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/twemoji.min.js', 'https://unpkg.com/[email protected]/dist/twemoji.min.js' ] } ]; for (const lib of libs) { if (lib.check()) continue; // already present let loaded = false; for (const url of lib.urls) { try { await loadScript(url); if (lib.check()) { loaded = true; break; } } catch (e) { // try next } } if (!loaded) { throw new Error(`Could not load ${lib.name} from any CDN.`); } } } // Boot once libs are ready loadLibraries().then(initApp).catch(err => { console.error('[GodMode Notepad] Failed to load libs:', err); alert('GodMode Notepad: Failed to load required libraries. Check console.'); }); // ------------------------ Main App ------------------------ function initApp() { // ------------------------ Shadow DOM Wrapper ------------------------ const container = document.createElement('div'); container.id = 'godmode-notepad-root'; document.documentElement.appendChild(container); const shadow = container.attachShadow({ mode: 'open' }); // ------------------------ CSS ------------------------ const style = document.createElement('style'); style.textContent = ` :host { all: initial; font-family: 'Inter', 'Segoe UI', Arial, sans-serif; } :root, [data-theme="dark"] { --notepad-blur: 18px; --notepad-border: rgba(255,255,255,0.3); --notepad-glow: 0 8px 32px 0 rgba(31, 38, 135, 0.28); --notepad-radius: 1.5rem; --notepad-color-main: #ff2e63; --notepad-color-bg: #212733dd; --notepad-color-text: #f5f6fa; --notepad-color-btn: #00f5d4; --notepad-color-btn-txt: #192a56; --notepad-color-accent: #111; --notepad-transition: .18s cubic-bezier(.85,0,.15,1); --notepad-shadow: 0 4px 28px 6px rgba(31,38,135,0.15); --mark-bg: #ffff99; --mark-fg: #333; } [data-theme="light"] { --notepad-color-bg: #f0f0f8ee; --notepad-color-text: #1d1d1f; --notepad-border: rgba(0,0,0,0.25); --notepad-glow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); --notepad-color-btn: #0044ff; --notepad-color-btn-txt: #fff; --notepad-color-main: #ff006e; --mark-bg: #ffe680; --mark-fg: #000; } [data-theme="neon"] { --notepad-color-bg: #090915f0; --notepad-color-text: #d1fff9; --notepad-border: rgba(0,255,213,0.4); --notepad-glow: 0 0 18px rgba(0,255,213,0.6); --notepad-color-btn: #ff00ff; --notepad-color-btn-txt: #000; --notepad-color-main: #00f5d4; --mark-bg: #0ff; --mark-fg: #000; } [data-theme="colorblind"] { --notepad-color-bg: #2a2a2aee; --notepad-color-text: #f0f0f0; --notepad-border: #bcbcbc; --notepad-color-btn: #ffcc00; --notepad-color-btn-txt: #000; --notepad-color-main: #00bcd4; --mark-bg: #ff0; --mark-fg: #000; } .notepad-superpanel { position: fixed; z-index: 999999; top: 0; right: 0; width: 460px; max-width: 90vw; height: 100vh; background: var(--notepad-color-bg); color: var(--notepad-color-text); border-left: 2.5px solid var(--notepad-border); border-radius: var(--notepad-radius) 0 0 var(--notepad-radius); box-shadow: var(--notepad-glow), var(--notepad-shadow); backdrop-filter: blur(var(--notepad-blur)); transition: transform var(--notepad-transition), box-shadow var(--notepad-transition), opacity .25s; transform: translateX(110%); display: flex; flex-direction: column; overflow: hidden; user-select: text; } .notepad-superpanel.open { transform: translateX(0); box-shadow: 0 6px 48px 8px #0ff2; } .notepad-superpanel.overlay { width: 80%; height: 80%; border-radius: 1.5rem; border: 2.5px solid var(--notepad-border); left: 50%; top: 50%; transform: translate(-50%, -50%) scale(0.92); opacity: 0; } .notepad-superpanel.overlay.open { transform: translate(-50%, -50%) scale(1); opacity: 1; } .notepad-superpanel.shrunk { width: 320px; } .notepad-superpanel.floating { top:6vh; right:6vw; border-radius:2rem; height: 80vh; } .notepad-superpanel.left { left: 0; right: auto; border-radius: 0 var(--notepad-radius) var(--notepad-radius) 0; border-left:none; border-right: 2.5px solid var(--notepad-border); } .notepad-header { display:flex; align-items:center; gap:1rem; justify-content:space-between; padding:1.2rem 1.4rem 1.2rem 1.1rem; background:linear-gradient(110deg,rgba(0,0,0,0.13) 80%, var(--notepad-color-main) 120%); font-size:1.2rem; font-weight:700; letter-spacing: 0.01em; user-select: none; cursor: move; } .notepad-controls {display:flex;gap:0.7rem;flex-wrap:wrap;} .notepad-content {flex:1;display:flex;flex-direction:row;gap:0.4rem;overflow:hidden;} .notepad-editor-pane,.notepad-preview-pane{ flex:1; min-width:0; min-height:0; display:flex; flex-direction:column; background:transparent; overflow:auto; padding:1rem 0.8rem; transition:background var(--notepad-transition); } .notepad-editor-pane { background:rgba(35,35,40,0.2); border-radius:1.1rem 0 0 1.1rem; border-right:1.5px solid rgba(255,255,255,0.09); position:relative; } .notepad-preview-pane { background:rgba(60,80,80,0.14); border-radius:0 1.1rem 1.1rem 0; border-left:1.5px solid rgba(255,255,255,0.09); } .notepad-textarea { width:100%;height:100%;resize:none;border:none;outline:none;background:transparent;color:var(--notepad-color-text); font-size:1.05rem;line-height:1.7;font-family:inherit;box-shadow:none; } .notepad-markdown-preview { width:100%;min-height:100px; color:var(--notepad-color-text); font-size:1.08rem; line-height:1.7; } .notepad-footer { padding:0.9rem 1.4rem; display:flex;align-items:center;justify-content:space-between; flex-wrap:wrap; background:rgba(30,40,60,0.17); border-top: 1.5px solid var(--notepad-border); font-size:0.95rem;user-select: none; gap:0.4rem; } .notepad-theme-selector, .notepad-btn, .notepad-note-selector, .notepad-search { border:none; border-radius:0.7rem; outline:none; padding:0.38rem 1.1rem; font-weight:600; background:var(--notepad-color-btn); color:var(--notepad-color-btn-txt); cursor:pointer; transition:filter .17s; } .notepad-note-selector, .notepad-search { background:#333;color:#fff;padding:0.3rem 0.8rem; } .notepad-btn:active { filter:brightness(0.85);} .notepad-btn.save { background:var(--notepad-color-main);color:white;} .notepad-btn.emoji { padding:0.3rem 0.7rem;font-size:1.15rem; } .notepad-btn.shrink { background:#1f1f1faa;color:#00f5d4;} .notepad-btn.float { background:#00f5d430;color:#ff2e63;} .notepad-btn.left { background:#1919a3cc;color:white;} .notepad-btn.split { background:#212733;color:#fff;} .notepad-btn.help { background:#fff;color:#222;} .notepad-btn.mode { background:#ffcc00;color:#333;} .notepad-btn.fs { background:#66ccff;color:#333;} .notepad-btn.del {background:#e74c3c;color:#fff;} .notepad-btn.export, .notepad-btn.import, .notepad-btn.new, .notepad-btn.dup, .notepad-btn.rename, .notepad-btn.history, .notepad-btn.restore { font-size:0.92rem;} .notepad-shortcut-badge { background:#333;color:#fff;border-radius:0.6rem;padding:0.12rem 0.5rem;font-size:0.75em;margin-left:0.25rem; } .notepad-toast { position:fixed;top:9vh;right:6vw;z-index:999999; background:rgba(30,30,30,0.92);color:#fffa;padding:1rem 1.7rem;border-radius:1.2rem;font-size:1.08rem;font-weight:500;box-shadow:0 8px 32px #00f5d430;pointer-events:none;animation:fadeIn 0.22s cubic-bezier(.85,0,.15,1); } @keyframes fadeIn{0%{opacity:0;transform:translateY(-8px);}100%{opacity:1;}} .notepad-unsaved { animation: pulse 1.2s infinite alternate; } @keyframes pulse { to { filter: brightness(1.3) drop-shadow(0 0 8px #ff2e63) } } .notepad-flag-bar { height:5px;width:100%;background: linear-gradient(90deg,#FF9933 33%,#FFF 33% 66%,#138808 66%); margin-bottom:-4px; } .notepad-emoji-picker {position:absolute;bottom:2.2rem;left:1rem;z-index:5;background:#222c;border-radius:1.1rem;padding:0.45rem 0.7rem;display:grid;grid-template-columns:repeat(7,1fr);gap:0.33rem;box-shadow:0 2px 12px #0003;} .notepad-emoji {font-size:1.28rem;cursor:pointer;padding:0.22rem;border-radius:0.4rem;transition:background .12s;} .notepad-emoji:hover {background:rgba(0,245,212,0.2);} mark { background: var(--mark-bg); color: var(--mark-fg); } .toggle-button { position:fixed;top:14px;right:17px;z-index:999998; background:var(--notepad-color-main);color:#fff;border:none;border-radius:1.3rem; padding:0.66rem 1.8rem;font-weight:700;font-size:1.13rem;box-shadow:0 2px 20px #ff2e6323; cursor:pointer;transition:filter .14s; } .notepad-history-list { max-height: 50vh; overflow:auto; background:#111c;color:#fff;padding:0.8rem;border-radius:0.7rem;margin-top:0.6rem; } .notepad-history-item { display:flex;justify-content:space-between;align-items:center;gap:0.6rem;background:#222a;padding:0.35rem 0.5rem;border-radius:0.5rem;margin-bottom:0.35rem;font-size:0.88rem; } .notepad-history-item button { padding:0.2rem 0.5rem;border-radius:0.4rem;background:var(--notepad-color-btn);color:var(--notepad-color-btn-txt);border:none;cursor:pointer;font-size:0.8rem; } .notepad-modal { position:fixed;inset:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:9999999; } .notepad-modal-box { background:#222e;color:#fff;padding:1rem 1.4rem;border-radius:1rem;min-width:300px;max-width:80vw;max-height:80vh;overflow:auto;box-shadow:0 4px 28px #0008; } .notepad-modal-box h3 { margin-top:0;margin-bottom:0.7rem;font-size:1.2rem;} .notepad-modal-box input[type="text"] { width:100%;padding:0.5rem;border-radius:0.5rem;border:none;margin:0.5rem 0;background:#333;color:#fff; } .notepad-modal-buttons {display:flex;gap:0.6rem;justify-content:flex-end;margin-top:0.9rem;} .notepad-modal-buttons button { padding:0.45rem 0.9rem;border:none;border-radius:0.5rem;cursor:pointer;font-weight:600; } .np-btn-ok { background:var(--notepad-color-btn);color:var(--notepad-color-btn-txt);} .np-btn-cancel { background:#666;color:#fff;} `; shadow.appendChild(style); // ------------------------ HTML ------------------------ const panel = document.createElement('div'); panel.className = 'notepad-superpanel side'; panel.setAttribute('data-theme', localStorage.getItem('notepad_theme') || 'dark'); const flagbar = document.createElement('div'); flagbar.className = 'notepad-flag-bar'; const header = document.createElement('div'); header.className = 'notepad-header'; header.innerHTML = `<span>✍️ Character.AI Notepad <span class="notepad-shortcut-badge">Ctrl+Shift+N</span></span>`; const noteSelector = document.createElement('select'); noteSelector.className = 'notepad-note-selector'; header.appendChild(noteSelector); const controls = document.createElement('div'); controls.className = 'notepad-controls'; header.appendChild(controls); const mkBtn = (cls, txt, title) => { const b = document.createElement('button'); b.className = `notepad-btn ${cls}`; b.textContent = txt; if (title) b.title = title; return b; }; const shrinkBtn = mkBtn('shrink', '⇔', 'Shrink Panel'); const floatBtn = mkBtn('float', '↗', 'Float Panel'); const leftBtn = mkBtn('left', '⇤', 'Stick Left'); const splitBtn = mkBtn('split', '⎚', 'Toggle Split View'); const helpBtn = mkBtn('help', '?', 'Help / Shortcuts'); const modeBtn = mkBtn('mode', '↔', 'Side/Overlay Mode'); const fsBtn = mkBtn('fs', '⛶', 'Full Screen'); controls.append(shrinkBtn, floatBtn, leftBtn, splitBtn, helpBtn, modeBtn, fsBtn); const content = document.createElement('div'); content.className = 'notepad-content'; const editorPane = document.createElement('div'); editorPane.className = 'notepad-editor-pane'; const textarea = document.createElement('textarea'); textarea.className = 'notepad-textarea'; textarea.spellcheck = true; textarea.autocorrect = 'on'; const emojiBtn = mkBtn('emoji', '😊', 'Emoji Picker'); let emojiPicker = null; const previewPane = document.createElement('div'); previewPane.className = 'notepad-preview-pane'; const mdPreview = document.createElement('div'); mdPreview.className = 'notepad-markdown-preview'; previewPane.appendChild(mdPreview); editorPane.append(textarea, emojiBtn); content.append(editorPane, previewPane); const footer = document.createElement('div'); footer.className = 'notepad-footer'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search notes...'; searchInput.className = 'notepad-search'; const saveBtn = mkBtn('save', '💾 Save (Ctrl+S)'); const newBtn = mkBtn('new', '➕ New'); const dupBtn = mkBtn('dup', '📄 Duplicate'); const renameBtn = mkBtn('rename', '✏️ Rename'); const delBtn = mkBtn('del', '🗑 Delete'); const expBtn = mkBtn('export', '⭳ Export'); const impBtn = mkBtn('import', '⭱ Import'); const histBtn = mkBtn('history', '🕒 History'); const themeSelector = document.createElement('select'); themeSelector.className = 'notepad-theme-selector'; themeSelector.innerHTML = ` <option value="dark">🌒 Dark</option> <option value="light">☀️ Light</option> <option value="neon">🌈 Neon</option> <option value="colorblind">🦉 Colorblind</option> `; const statBox = document.createElement('span'); statBox.style.fontSize = '0.95em'; footer.append( searchInput, saveBtn, newBtn, dupBtn, renameBtn, delBtn, expBtn, impBtn, histBtn, themeSelector, statBox ); panel.append(flagbar, header, content, footer); shadow.appendChild(panel); const tglBtn = document.createElement('button'); tglBtn.className = 'toggle-button'; tglBtn.textContent = '📝 Notepad'; shadow.appendChild(tglBtn); // ------------------------ Util ------------------------ function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } function getCharacterId() { const parts = location.pathname.split('/'); if (parts.includes('chat')) { const idx = parts.indexOf('chat'); return parts[idx + 1] || null; } return null; } function noteKey(cid) { return `charai_supernotepad_${cid || 'global'}`; } function histKey(cid) { return `charai_supernotepad_history_${cid || 'global'}`; } function compress(obj) { return LZString.compressToUTF16(JSON.stringify(obj)); } function decompress(str) { if (!str) return null; try { return JSON.parse(LZString.decompressFromUTF16(str) || 'null'); } catch (e) { return null; } } function saveNotes(cid, notesData) { localStorage.setItem(noteKey(cid), compress(notesData)); } function loadNotes(cid) { let raw = localStorage.getItem(noteKey(cid)); let data = decompress(raw); if (!data || !data.notes) data = { notes: [] }; return data; } function saveHistory(cid, snapshots) { localStorage.setItem(histKey(cid), compress(snapshots)); } function loadHistory(cid) { let raw = localStorage.getItem(histKey(cid)); let data = decompress(raw); if (!data || !data.snaps) data = { snaps: [] }; return data; } function showToast(msg, time = 2200) { let t = document.createElement('div'); t.className = 'notepad-toast'; t.textContent = msg; shadow.appendChild(t); setTimeout(() => { t.remove(); }, time); } function downloadFile(name, content, type = 'text/plain') { let a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([content], { type })); a.download = name; shadow.appendChild(a); a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); a.remove(); } function uploadFile(cb) { let i = document.createElement('input'); i.type = 'file'; i.accept = '.txt,.md,.json'; i.onchange = (e) => { let file = e.target.files[0]; if (file) { let r = new FileReader(); r.onload = () => cb(r.result, file.name); r.readAsText(file); } }; shadow.appendChild(i); i.click(); i.remove(); } function setTheme(theme) { panel.setAttribute('data-theme', theme); localStorage.setItem('notepad_theme', theme); } function createModal(html, onOk, onCancel) { const modal = document.createElement('div'); modal.className = 'notepad-modal'; const box = document.createElement('div'); box.className = 'notepad-modal-box'; box.innerHTML = html; const btns = document.createElement('div'); btns.className = 'notepad-modal-buttons'; const ok = document.createElement('button'); ok.textContent = 'OK'; ok.className = 'np-btn-ok'; const cancel = document.createElement('button'); cancel.textContent = 'Cancel'; cancel.className = 'np-btn-cancel'; btns.append(ok, cancel); box.appendChild(btns); modal.appendChild(box); shadow.appendChild(modal); ok.onclick = () => { if (onOk) onOk(modal); modal.remove(); }; cancel.onclick = () => { if (onCancel) onCancel(modal); modal.remove(); }; return modal; } // ------------------------ State ------------------------ let noteState = { charId: getCharacterId() || 'global', notes: [], currentNoteIndex: 0, split: true, dirty: false, searchQuery: '', history: { snaps: [] } }; // ------------------------ Core Logic ------------------------ function loadCurrentNote() { noteState.charId = getCharacterId() || 'global'; let data = loadNotes(noteState.charId); noteState.notes = data.notes || []; noteState.split = data.split !== undefined ? data.split : true; if (noteState.notes.length === 0) { noteState.notes.push({ title: 'Note 1', content: '', ts: Date.now() }); } noteState.currentNoteIndex = Math.min(noteState.currentNoteIndex, noteState.notes.length - 1); updateNoteSelector(); loadSelectedNote(); noteState.history = loadHistory(noteState.charId); } function loadSelectedNote() { let note = noteState.notes[noteState.currentNoteIndex]; textarea.value = note.content; updatePreview(); updateStats(); } function updateNoteSelector() { noteSelector.innerHTML = ''; noteState.notes.forEach((note, index) => { let option = document.createElement('option'); option.value = index; option.textContent = note.title; noteSelector.appendChild(option); }); noteSelector.value = noteState.currentNoteIndex; } function saveCurrentNote(showToastMsg = true) { let note = noteState.notes[noteState.currentNoteIndex]; note.content = textarea.value; note.ts = Date.now(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); noteState.dirty = false; panel.classList.remove('notepad-unsaved'); if (showToastMsg) showToast('✅ Note saved.'); updateStats(); } function snapshotHistory() { const note = noteState.notes[noteState.currentNoteIndex]; noteState.history.snaps.push({ idx: noteState.currentNoteIndex, title: note.title, content: note.content, ts: Date.now() }); if (noteState.history.snaps.length > 50) noteState.history.snaps.shift(); saveHistory(noteState.charId, noteState.history); } const debouncedSnapshot = debounce(snapshotHistory, 60000); function updatePreview() { let text = textarea.value || ''; const html = DOMPurify.sanitize(marked.parse(text)); mdPreview.innerHTML = html; // Search highlight AFTER render if (noteState.searchQuery) { const q = noteState.searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(q, 'gi'); const walker = document.createTreeWalker(mdPreview, NodeFilter.SHOW_TEXT, null); const nodes = []; let node; while ((node = walker.nextNode())) nodes.push(node); nodes.forEach(n => { const txt = n.nodeValue; if (regex.test(txt)) { const span = document.createElement('span'); span.innerHTML = txt.replace(regex, m => `<mark>${m}</mark>`); n.parentNode.replaceChild(span, n); } }); } if (window.twemoji) window.twemoji.parse(mdPreview); noteState.dirty = textarea.value !== noteState.notes[noteState.currentNoteIndex].content; panel.classList.toggle('notepad-unsaved', noteState.dirty); updateStats(); debouncedSnapshot(); } function updateStats() { let wc = (textarea.value.trim().match(/\S+/g) || []).length; let charCount = textarea.value.length; let note = noteState.notes[noteState.currentNoteIndex]; let date = note.ts ? new Date(note.ts).toLocaleString() : ''; statBox.textContent = `Words: ${wc} | Chars: ${charCount} | Last saved: ${date}`; if (noteState.dirty) statBox.textContent += ' [unsaved]'; } function confirmDelete() { if (noteState.notes.length <= 1) { if (confirm('This is the only note. Delete it?')) { noteState.notes = [{ title: 'Note 1', content: '', ts: Date.now() }]; noteState.currentNoteIndex = 0; updateNoteSelector(); loadSelectedNote(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('🗑 Note deleted. Created a new one.'); } } else { if (confirm('Delete current note?')) { noteState.notes.splice(noteState.currentNoteIndex, 1); if (noteState.currentNoteIndex >= noteState.notes.length) { noteState.currentNoteIndex = noteState.notes.length - 1; } updateNoteSelector(); loadSelectedNote(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('🗑 Note deleted.'); } } } function createNewNote() { if (noteState.dirty && !confirm('You have unsaved changes! Discard and start new?')) return; const modal = createModal(` <h3>New Note</h3> <input type="text" id="np-new-title" placeholder="Note title"> `, (m) => { const val = m.querySelector('#np-new-title').value.trim(); const title = val || `Note ${noteState.notes.length + 1}`; noteState.notes.push({ title, content: '', ts: Date.now() }); noteState.currentNoteIndex = noteState.notes.length - 1; updateNoteSelector(); loadSelectedNote(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('🆕 New note created.'); }); modal.querySelector('#np-new-title').focus(); } function duplicateNote() { const src = noteState.notes[noteState.currentNoteIndex]; const clone = { title: src.title + ' (copy)', content: src.content, ts: Date.now() }; noteState.notes.splice(noteState.currentNoteIndex + 1, 0, clone); noteState.currentNoteIndex++; updateNoteSelector(); loadSelectedNote(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('📄 Note duplicated.'); } function renameNote() { const current = noteState.notes[noteState.currentNoteIndex]; const modal = createModal(` <h3>Rename Note</h3> <input type="text" id="np-rename-title" value="${current.title}"> `, (m) => { const val = m.querySelector('#np-rename-title').value.trim(); if (val) { current.title = val; updateNoteSelector(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('✏️ Note renamed.'); } }); modal.querySelector('#np-rename-title').focus(); } function exportNotes() { let all = {}; for (let k in localStorage) { if (k.startsWith('charai_supernotepad_') && !k.includes('_history_')) { all[k] = localStorage[k]; } } downloadFile('characterai_notepad.json', JSON.stringify(all, null, 2), 'application/json'); showToast('⭳ All notes exported as JSON.'); } function exportCurrentMd() { const note = noteState.notes[noteState.currentNoteIndex]; const fname = `charai_${noteState.charId}_${note.title}.md`.replace(/[^\w.-]+/g, '_'); downloadFile(fname, note.content, 'text/markdown'); showToast('⭳ Exported as .md'); } function importNotes() { uploadFile((txt, filename) => { let isJson = false; let data = null; try { data = JSON.parse(txt); isJson = true; } catch {} if (isJson && data.notes) { noteState.notes = data.notes; noteState.currentNoteIndex = 0; updateNoteSelector(); loadSelectedNote(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('⭱ Notes imported (JSON).'); } else if (isJson && Object.keys(data).some(k => k.startsWith('charai_supernotepad_'))) { Object.keys(data).forEach(k => localStorage.setItem(k, data[k])); loadCurrentNote(); showToast('⭱ Bulk import completed.'); } else { const modal = createModal(` <h3>Import as New Note</h3> <p>File: ${filename}</p> <input type="text" id="np-import-title" placeholder="Imported Note Title" value="Imported Note"> `, (m) => { const newTitle = m.querySelector('#np-import-title').value.trim() || 'Imported Note'; noteState.notes.push({ title: newTitle, content: txt, ts: Date.now() }); noteState.currentNoteIndex = noteState.notes.length - 1; updateNoteSelector(); loadSelectedNote(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('⭱ Imported as new note.'); }); modal.querySelector('#np-import-title').focus(); } }); } function showHelp() { let msg = `Keyboard shortcuts: Ctrl+Shift+N = Toggle Notepad Ctrl+S = Save Ctrl+Shift+M = Export current as .md Ctrl+Shift+E = Export all (JSON) Ctrl+Shift+I = Import Ctrl+Shift+L = Cycle Theme Ctrl+Shift+F = Toggle Floating Ctrl+Shift+R = Rename Note Ctrl+Shift+D = Duplicate Note Ctrl+Shift+H = History Ctrl+Shift+S = Shrink Panel Ctrl+Shift+O = Side/Overlay Mode - Autosave on idle - Markdown + emoji supported - Multiple notes per character - Search highlight after render - Shadow DOM isolation - DOMPurify XSS sanitization - History snapshots every ~60s while typing `; alert(msg); } function togglePanel(force) { const isOpen = force === true || (force === undefined && !panel.classList.contains('open')); const isOverlay = panel.classList.contains('overlay'); panel.classList.toggle('open', isOpen); if (isOpen) loadCurrentNote(); if (isOverlay && isOpen) { panel.style.left = '50%'; panel.style.top = '50%'; } } function showHistoryModal() { const hist = noteState.history.snaps.slice().reverse(); if (!hist.length) { showToast('No history snapshots yet.'); return; } const listHtml = hist.map((snap, idx) => { const date = new Date(snap.ts).toLocaleString(); return ` <div class="notepad-history-item" data-idx="${hist.length - 1 - idx}"> <span>${snap.title} — <em>${date}</em></span> <button class="np-restore">Restore</button> </div>`; }).join(''); const modal = createModal(` <h3>History (last ${hist.length})</h3> <div class="notepad-history-list">${listHtml}</div> `, null, null); modal.querySelectorAll('.np-restore').forEach(btn => { btn.onclick = (e) => { const parent = e.target.closest('.notepad-history-item'); const idx = parseInt(parent.dataset.idx, 10); const snap = noteState.history.snaps[idx]; if (!snap) return; const c = confirm('Restore this snapshot? This will overwrite current note content.'); if (c) { if (snap.idx >= noteState.notes.length) { noteState.notes.push({ title: snap.title, content: snap.content, ts: Date.now() }); noteState.currentNoteIndex = noteState.notes.length - 1; } else { noteState.currentNoteIndex = snap.idx; noteState.notes[snap.idx].title = snap.title; noteState.notes[snap.idx].content = snap.content; noteState.notes[snap.idx].ts = Date.now(); } updateNoteSelector(); loadSelectedNote(); saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); showToast('⏪ Snapshot restored.'); modal.remove(); } }; }); } // ------------------------ Event Bindings ------------------------ noteSelector.onchange = () => { if (noteState.dirty) { if (!confirm('You have unsaved changes. Switch note anyway?')) { noteSelector.value = noteState.currentNoteIndex; return; } } noteState.currentNoteIndex = parseInt(noteSelector.value, 10); loadSelectedNote(); }; saveBtn.onclick = () => saveCurrentNote(); newBtn.onclick = () => createNewNote(); dupBtn.onclick = () => duplicateNote(); renameBtn.onclick = () => renameNote(); delBtn.onclick = () => confirmDelete(); expBtn.onclick = () => exportNotes(); impBtn.onclick = () => importNotes(); histBtn.onclick = () => showHistoryModal(); shrinkBtn.onclick = () => panel.classList.toggle('shrunk'); floatBtn.onclick = () => panel.classList.toggle('floating'); leftBtn.onclick = () => panel.classList.toggle('left'); splitBtn.onclick = () => { noteState.split = !noteState.split; content.style.flexDirection = noteState.split ? 'row' : 'column'; saveNotes(noteState.charId, { notes: noteState.notes, split: noteState.split }); }; helpBtn.onclick = () => showHelp(); modeBtn.onclick = () => { const isSide = panel.classList.contains('side'); panel.classList.toggle('side', !isSide); panel.classList.toggle('overlay', isSide); }; fsBtn.onclick = () => { if (document.fullscreenElement) { document.exitFullscreen(); } else { panel.requestFullscreen(); } }; tglBtn.onclick = () => togglePanel(); themeSelector.value = localStorage.getItem('notepad_theme') || 'dark'; setTheme(themeSelector.value); themeSelector.onchange = () => setTheme(themeSelector.value); const debouncedSave = debounce(() => saveCurrentNote(false), 4000); textarea.oninput = () => { updatePreview(); debouncedSave(); }; textarea.onblur = () => { updatePreview(); saveCurrentNote(false); }; searchInput.oninput = () => { noteState.searchQuery = searchInput.value.trim(); updatePreview(); }; // Emoji picker emojiBtn.onclick = () => { if (emojiPicker) { emojiPicker.remove(); emojiPicker = null; return; } emojiPicker = document.createElement('div'); emojiPicker.className = 'notepad-emoji-picker'; "😀😄😎😜🤔🤯😭😡🔥🎉💡📌✨🚀👀🤖🤷🧠💀🙃🙄😂🥺👽💩🤡👑🏆🔒🦾🔑🧃".split('').forEach(e => { let eb = document.createElement('span'); eb.className = 'notepad-emoji'; eb.textContent = e; eb.onclick = () => { const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.setRangeText(e, start, end, 'end'); textarea.focus(); updatePreview(); }; emojiPicker.append(eb); }); editorPane.append(emojiPicker); const closePicker = (ev) => { if (!emojiPicker.contains(ev.composedPath()[0]) && ev.target !== emojiBtn) { emojiPicker.remove(); emojiPicker = null; shadow.removeEventListener('click', closePicker, true); } }; shadow.addEventListener('click', closePicker, true); }; // Dragging overlay let isDragging = false; let startX, startY, startLeft, startTop; header.onpointerdown = (e) => { if (panel.classList.contains('overlay')) { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; header.setPointerCapture(e.pointerId); } }; header.onpointermove = (e) => { if (isDragging) { let dx = e.clientX - startX; let dy = e.clientY - startY; panel.style.left = (startLeft + dx) + 'px'; panel.style.top = (startTop + dy) + 'px'; } }; header.onpointerup = (e) => { if (isDragging) { isDragging = false; header.releasePointerCapture(e.pointerId); } }; // Keyboard Shortcuts window.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.code === 'KeyN') { togglePanel(); e.preventDefault(); } if (e.ctrlKey && !e.shiftKey && e.code === 'KeyS') { saveCurrentNote(); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyM') { exportCurrentMd(); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyE') { exportNotes(); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyI') { importNotes(); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyL') { let t = themeSelector.selectedIndex + 1; if (t >= themeSelector.options.length) t = 0; themeSelector.selectedIndex = t; setTheme(themeSelector.value); showToast(`Theme: ${themeSelector.value}`); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyF') { panel.classList.toggle('floating'); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyS') { panel.classList.toggle('shrunk'); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyO') { modeBtn.click(); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyR') { renameBtn.click(); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyD') { dupBtn.click(); e.preventDefault(); } if (e.ctrlKey && e.shiftKey && e.code === 'KeyH') { histBtn.click(); e.preventDefault(); } }); window.addEventListener('beforeunload', e => { if (panel.classList.contains('open') && noteState.dirty) { e.preventDefault(); e.returnValue = 'You have unsaved notepad changes!'; return 'You have unsaved notepad changes!'; } }); // Watch for route changes let lastPath = location.pathname; setInterval(() => { if (location.pathname !== lastPath) { lastPath = location.pathname; let cid = getCharacterId() || 'global'; if (cid !== noteState.charId) loadCurrentNote(); } }, 700); // Cross-tab sync window.addEventListener('storage', (e) => { if (e.key === noteKey(noteState.charId)) { const curNote = noteState.notes[noteState.currentNoteIndex]; const wasDirty = noteState.dirty; const oldContent = textarea.value; loadCurrentNote(); if (wasDirty && curNote && textarea.value !== oldContent) { showToast('⚠️ Notes changed in another tab.'); } } }); // Easter egg flagbar.onclick = () => window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank'); // Initial load setTimeout(() => { loadCurrentNote(); }, 300); } })();