| <!DOCTYPE html> |
|
|
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Paper</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" |
| rel="stylesheet"> |
| <style> |
| :root { |
| |
| --bg-primary: #0f0f1a; |
| --bg-secondary: #1a1a2e; |
| --bg-tertiary: #252540; |
| --bg-card: rgba(25, 25, 45, 0.95); |
| --text-primary: #ffffff; |
| --text-secondary: #b8b8d0; |
| --text-muted: #7070a0; |
| --accent-pink: #ff6b9d; |
| --accent-purple: #a855f7; |
| --accent-blue: #3b82f6; |
| --accent-yellow: #fbbf24; |
| --gradient-primary: linear-gradient(135deg, #ff6b9d 0%, #a855f7 50%, #3b82f6 100%); |
| --gradient-hover: linear-gradient(135deg, #ff4d8a 0%, #9333ea 50%, #2563eb 100%); |
| --success: #22c55e; |
| --warning: #f59e0b; |
| --error: #ef4444; |
| --border: rgba(168, 85, 247, 0.25); |
| --shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.3); |
| --shadow-glow: 0 8px 40px rgba(168, 85, 247, 0.15); |
| --radius: 24px; |
| --radius-sm: 16px; |
| --transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| html, |
| body { |
| height: 100%; |
| } |
| |
| body { |
| font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif; |
| background: linear-gradient(135deg, #0f0f1a 0%, #1a1025 50%, #0f1520 100%); |
| background-attachment: fixed; |
| color: var(--text-primary); |
| line-height: 1.6; |
| position: relative; |
| } |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| top: -50%; |
| left: -50%; |
| width: 200%; |
| height: 200%; |
| background: |
| radial-gradient(circle at 20% 30%, rgba(255, 107, 157, 0.25) 0%, transparent 40%), |
| radial-gradient(circle at 80% 70%, rgba(59, 130, 246, 0.2) 0%, transparent 40%), |
| radial-gradient(circle at 50% 50%, rgba(168, 85, 247, 0.15) 0%, transparent 50%); |
| animation: floatBg 20s ease-in-out infinite; |
| pointer-events: none; |
| z-index: 1; |
| } |
| |
| @keyframes floatBg { |
| |
| 0%, |
| 100% { |
| transform: translate(0, 0) rotate(0deg); |
| } |
| |
| 33% { |
| transform: translate(2%, 2%) rotate(1deg); |
| } |
| |
| 66% { |
| transform: translate(-1%, 1%) rotate(-1deg); |
| } |
| } |
| |
| |
| .login-screen { |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| height: 100%; |
| padding: 20px; |
| position: relative; |
| z-index: 2; |
| } |
| |
| |
| .card-container { |
| width: 100%; |
| max-width: 420px; |
| perspective: 1000px; |
| } |
| |
| .card-inner { |
| position: relative; |
| width: 100%; |
| transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); |
| transform-style: preserve-3d; |
| } |
| |
| .card-container.flipped .card-inner { |
| transform: rotateY(180deg); |
| } |
| |
| .card-front, |
| .card-back { |
| text-align: center; |
| padding: 40px 36px; |
| border-radius: var(--radius); |
| background: var(--bg-card); |
| border: 2px solid var(--border); |
| box-shadow: var(--shadow-soft), var(--shadow-glow); |
| backface-visibility: hidden; |
| -webkit-backface-visibility: hidden; |
| } |
| |
| .card-front { |
| position: relative; |
| z-index: 2; |
| animation: bounceIn 0.6s var(--transition); |
| } |
| |
| .card-back { |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| transform: rotateY(180deg); |
| z-index: 1; |
| } |
| |
| @keyframes bounceIn { |
| 0% { |
| opacity: 0; |
| transform: scale(0.9) translateY(20px); |
| } |
| |
| 50% { |
| transform: scale(1.02) translateY(-5px); |
| } |
| |
| 100% { |
| opacity: 1; |
| transform: scale(1) translateY(0); |
| } |
| } |
| |
| .login-box h1 { |
| margin-bottom: 12px; |
| font-weight: 800; |
| font-size: 42px; |
| letter-spacing: -1px; |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| background: var(--gradient-primary); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .login-box h1::after { |
| content: ' ✨'; |
| -webkit-text-fill-color: initial; |
| } |
| |
| .login-subtitle { |
| color: var(--text-secondary); |
| font-size: 16px; |
| margin-bottom: 32px; |
| font-weight: 500; |
| line-height: 1.6; |
| } |
| |
| |
| .info-icon { |
| position: absolute; |
| top: 16px; |
| right: 16px; |
| width: 28px; |
| height: 28px; |
| border-radius: 50%; |
| background: var(--bg-tertiary); |
| border: 1px solid var(--border); |
| color: var(--text-muted); |
| font-size: 14px; |
| font-weight: 600; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| transition: var(--transition); |
| font-family: serif; |
| z-index: 5; |
| } |
| |
| .info-icon:hover { |
| background: var(--accent-purple); |
| color: white; |
| border-color: var(--accent-purple); |
| transform: scale(1.1); |
| } |
| |
| .back-icon { |
| position: absolute; |
| top: 16px; |
| right: 16px; |
| width: 28px; |
| height: 28px; |
| border-radius: 50%; |
| background: var(--bg-tertiary); |
| border: 1px solid var(--border); |
| color: var(--text-muted); |
| font-size: 18px; |
| font-weight: 300; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| transition: var(--transition); |
| line-height: 1; |
| } |
| |
| .back-icon:hover { |
| background: var(--accent-pink); |
| color: white; |
| border-color: var(--accent-pink); |
| transform: scale(1.1); |
| } |
| |
| .info-title { |
| font-size: 22px; |
| font-weight: 700; |
| color: var(--text-primary); |
| margin-bottom: 20px; |
| background: var(--gradient-primary); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .info-list { |
| list-style: none; |
| padding: 0; |
| margin: 0 0 24px 0; |
| text-align: left; |
| max-height: 200px; |
| overflow-y: auto; |
| padding-right: 8px; |
| } |
| |
| |
| .info-list::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .info-list::-webkit-scrollbar-track { |
| background: var(--bg-tertiary); |
| border-radius: 6px; |
| } |
| |
| .info-list::-webkit-scrollbar-thumb { |
| background: var(--gradient-primary); |
| border-radius: 6px; |
| } |
| |
| .info-list::-webkit-scrollbar-thumb:hover { |
| background: var(--gradient-hover); |
| } |
| |
| .info-list li { |
| color: var(--text-secondary); |
| font-size: 13px; |
| line-height: 1.7; |
| padding: 10px 0; |
| padding-left: 24px; |
| position: relative; |
| border-bottom: 1px solid var(--border); |
| } |
| |
| .info-list li:last-child { |
| border-bottom: none; |
| } |
| |
| .info-list li::before { |
| content: '✦'; |
| position: absolute; |
| left: 0; |
| color: var(--accent-purple); |
| } |
| |
| .info-list li strong { |
| color: var(--text-primary); |
| } |
| |
| .info-link { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| color: var(--accent-blue); |
| text-decoration: none; |
| font-weight: 600; |
| font-size: 13px; |
| transition: var(--transition); |
| padding: 10px 16px; |
| background: var(--bg-tertiary); |
| border-radius: 50px; |
| border: 1px solid var(--border); |
| } |
| |
| .info-link:hover { |
| color: white; |
| background: var(--accent-blue); |
| border-color: var(--accent-blue); |
| } |
| |
| .input-group { |
| position: relative; |
| margin-bottom: 24px; |
| } |
| |
| .password-input { |
| width: 100%; |
| padding: 18px 24px; |
| font-size: 17px; |
| border: 2px solid var(--border); |
| border-radius: var(--radius-sm); |
| background: var(--bg-secondary); |
| color: var(--text-primary); |
| text-align: center; |
| letter-spacing: 3px; |
| outline: none; |
| transition: var(--transition); |
| font-family: 'Plus Jakarta Sans', monospace; |
| font-weight: 600; |
| } |
| |
| .password-input:focus { |
| border-color: var(--accent-purple); |
| box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.25), 0 0 30px rgba(168, 85, 247, 0.15); |
| background: var(--bg-tertiary); |
| transform: scale(1.02); |
| } |
| |
| .password-input::placeholder { |
| color: var(--text-muted); |
| letter-spacing: normal; |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| font-weight: 500; |
| } |
| |
| .enter-btn { |
| width: 100%; |
| padding: 18px 32px; |
| background: var(--gradient-primary); |
| color: #ffffff; |
| border: none; |
| border-radius: var(--radius-sm); |
| font-size: 16px; |
| font-weight: 700; |
| cursor: pointer; |
| transition: var(--transition); |
| position: relative; |
| overflow: hidden; |
| text-transform: uppercase; |
| letter-spacing: 1.5px; |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| box-shadow: 0 6px 25px rgba(255, 107, 157, 0.35); |
| } |
| |
| .enter-btn:hover:not(:disabled) { |
| transform: translateY(-3px) scale(1.02); |
| box-shadow: 0 10px 35px rgba(255, 107, 157, 0.45); |
| background: var(--gradient-hover); |
| } |
| |
| .enter-btn:active { |
| transform: translateY(0) scale(0.98); |
| } |
| |
| .enter-btn:disabled { |
| opacity: 0.6; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .error { |
| color: var(--error); |
| margin-top: 16px; |
| font-size: 14px; |
| min-height: 20px; |
| font-weight: 500; |
| } |
| |
| |
| .editor-screen { |
| display: none; |
| height: 100%; |
| background: transparent; |
| flex-direction: column; |
| position: relative; |
| z-index: 2; |
| } |
| |
| .header { |
| padding: 16px 24px; |
| background: var(--bg-card); |
| border-bottom: 2px solid var(--border); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| backdrop-filter: blur(10px); |
| position: relative; |
| flex-shrink: 0; |
| } |
| |
| .header-left { |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| } |
| |
| .app-title { |
| font-size: 26px; |
| font-weight: 800; |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| letter-spacing: -0.5px; |
| background: var(--gradient-primary); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .app-title:hover { |
| transform: scale(1.05); |
| } |
| |
| .header-right { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| flex-wrap: wrap; |
| } |
| |
| .save-status { |
| font-size: 12px; |
| font-weight: 700; |
| padding: 10px 18px; |
| border-radius: 50px; |
| background: var(--bg-tertiary); |
| color: var(--text-secondary); |
| border: 2px solid var(--border); |
| min-width: 90px; |
| text-align: center; |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| transition: var(--transition); |
| } |
| |
| .save-status.saving { |
| background: var(--gradient-primary); |
| color: white; |
| border-color: transparent; |
| box-shadow: 0 4px 20px rgba(168, 85, 247, 0.3); |
| animation: wiggle 0.5s ease-in-out infinite; |
| } |
| |
| @keyframes wiggle { |
| |
| 0%, |
| 100% { |
| transform: rotate(-2deg); |
| } |
| |
| 50% { |
| transform: rotate(2deg); |
| } |
| } |
| |
| .save-status.saved { |
| background: linear-gradient(135deg, var(--success) 0%, #10b981 100%); |
| color: white; |
| border-color: transparent; |
| box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); |
| } |
| |
| .word-count { |
| font-size: 14px; |
| color: var(--text-secondary); |
| font-weight: 600; |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| background: var(--bg-tertiary); |
| padding: 6px 14px; |
| border-radius: 20px; |
| } |
| |
| .editor-container { |
| flex: 1; |
| position: relative; |
| margin: 20px; |
| border-radius: var(--radius); |
| background: var(--bg-card); |
| border: 2px solid var(--border); |
| box-shadow: var(--shadow-soft); |
| animation: paperUnfold 0.5s ease-out; |
| } |
| |
| .editor { |
| width: 100%; |
| height: 100%; |
| padding: 28px 32px; |
| border: none; |
| outline: none; |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| font-size: 17px; |
| font-weight: 500; |
| line-height: 1.9; |
| resize: none; |
| background: transparent; |
| color: var(--text-primary); |
| position: relative; |
| z-index: 2; |
| border-radius: var(--radius); |
| caret-color: var(--accent-purple); |
| } |
| |
| .editor::placeholder { |
| color: var(--text-muted); |
| font-weight: 500; |
| } |
| |
| |
| .editor::-webkit-scrollbar { |
| width: 10px; |
| } |
| |
| .editor::-webkit-scrollbar-track { |
| background: var(--bg-tertiary); |
| border-radius: 10px; |
| } |
| |
| .editor::-webkit-scrollbar-thumb { |
| background: var(--gradient-primary); |
| border-radius: 10px; |
| } |
| |
| .editor::-webkit-scrollbar-thumb:hover { |
| background: var(--gradient-hover); |
| } |
| |
| |
| .fade-in { |
| animation: fadeIn 0.4s ease-out; |
| } |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(20px) scale(0.98); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0) scale(1); |
| } |
| } |
| |
| |
| .password-input:focus, |
| .editor:focus { |
| outline: none; |
| } |
| |
| @keyframes slideUp { |
| from { |
| opacity: 0; |
| transform: translateY(30px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| @keyframes paperUnfold { |
| from { |
| opacity: 0; |
| transform: scale(0.95) rotateX(5deg); |
| } |
| |
| to { |
| opacity: 1; |
| transform: scale(1) rotateX(0deg); |
| } |
| } |
| |
| |
| .editor-container::after { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: var(--paper-texture); |
| opacity: 0.1; |
| pointer-events: none; |
| z-index: 1; |
| border-radius: var(--radius); |
| } |
| |
| |
| @media (max-width: 600px) { |
| .header { |
| padding: 12px 16px; |
| gap: 8px; |
| } |
| |
| .app-title { |
| font-size: 20px; |
| } |
| |
| .header-right { |
| flex-wrap: nowrap; |
| gap: 8px; |
| } |
| |
| .save-status { |
| font-size: 10px; |
| padding: 6px 12px; |
| min-width: 70px; |
| letter-spacing: 0; |
| } |
| |
| .word-count { |
| font-size: 11px; |
| padding: 4px 10px; |
| } |
| |
| .editor-container { |
| margin: 12px; |
| } |
| |
| .editor { |
| padding: 20px; |
| font-size: 16px; |
| } |
| |
| .card-front, |
| .card-back { |
| padding: 28px 20px; |
| } |
| |
| .card-front h1 { |
| font-size: 32px; |
| } |
| |
| .login-subtitle { |
| font-size: 14px; |
| margin-bottom: 24px; |
| } |
| |
| .info-list { |
| margin-bottom: 16px; |
| } |
| |
| .info-list li { |
| font-size: 13px; |
| padding: 6px 0; |
| padding-left: 20px; |
| } |
| |
| .info-title { |
| font-size: 16px; |
| margin-bottom: 12px; |
| } |
| |
| .info-link { |
| font-size: 11px; |
| padding: 8px 12px; |
| } |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div id="loginScreen" class="login-screen"> |
| <div id="cardContainer" class="card-container"> |
| <div class="card-inner"> |
| |
| <div class="card-front"> |
| <div class="info-icon" onclick="flipCard()" title="Learn more">i</div> |
| <h1>Paper</h1> |
| <p class="login-subtitle">Perfect for temporary notes and secure sharing. Deleted after two days. |
| </p> |
|
|
| <div class="input-group"> |
| <input type="password" id="passwordInput" class="password-input" |
| placeholder="min-8-char password" minlength="8" maxlength="100"> |
| </div> |
|
|
| <button onclick="login()" class="enter-btn">Enter</button> |
| <div id="loginError" class="error"></div> |
| </div> |
|
|
| |
| <div class="card-back"> |
| <div class="back-icon" onclick="flipCard()" title="Back to login">X</div> |
| <h3 class="info-title">About Paper ✨</h3> |
| <ul class="info-list"> |
| <li><strong>What is it?</strong> A secure notepad for temporary notes you can access from |
| anywhere.</li> |
| <li><strong>Encryption:</strong> Notes encrypted client-side with your password. Never sent to |
| server.</li> |
| <li><strong>Open Source:</strong> 100% open. Deployed on Hugging Face, Open for everyone to see. |
| </li> |
| <li><strong>Zero Access:</strong> Even the developer cannot read your notes. Only encrypted |
| blobs stored.</li> |
| <li><strong>Auto-Delete:</strong> Notes automatically deleted after 2 days of inactivity.</li> |
| <li><strong>Pro tip:</strong> Use a strong password! If someone guesses it... well, that's on |
| you 😅</li> |
| </ul> |
| <a href="https://github.com/jebin2/Paper" target="_blank" class="info-link"> |
| 🔗 View source on GitHub |
| </a> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="editorScreen" class="editor-screen"> |
| <div class="header"> |
| <div class="header-left"> |
| <div class="app-title" onclick="goToLogin()" style="cursor: pointer;" title="Go to login">Paper</div> |
| </div> |
| <div class="header-right"> |
| <div id="wordCount" class="word-count">0 words</div> |
| <div id="saveStatus" class="save-status">Ready</div> |
| </div> |
| </div> |
| <div class="editor-container"> |
| <textarea id="editor" class="editor" placeholder="Start typing..."></textarea> |
| </div> |
| </div> |
|
|
| <script> |
| let currentPassword = ''; |
| let currentSalt = null; |
| let fileHash = ''; |
| let saveTimeout = null; |
| let isWorking = false; |
| |
| const PBKDF2_ITERATIONS = 250000; |
| |
| |
| function flipCard() { |
| const container = document.getElementById('cardContainer'); |
| container.classList.toggle('flipped'); |
| } |
| |
| |
| function updateWordCount() { |
| const text = document.getElementById('editor').value; |
| const words = text.trim() ? text.trim().split(/\s+/).length : 0; |
| const chars = text.length; |
| document.getElementById('wordCount').textContent = `${words} words, ${chars} chars`; |
| } |
| |
| |
| async function generateFilenameHash(password) { |
| const encoder = new TextEncoder(); |
| const data = encoder.encode(password); |
| const hashBuffer = await crypto.subtle.digest('SHA-256', data); |
| const hashArray = Array.from(new Uint8Array(hashBuffer)); |
| return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16); |
| } |
| |
| async function deriveKey(password, salt) { |
| const encoder = new TextEncoder(); |
| const keyMaterial = await crypto.subtle.importKey( |
| 'raw', |
| encoder.encode(password), |
| { name: 'PBKDF2' }, |
| false, |
| ['deriveKey'] |
| ); |
| return crypto.subtle.deriveKey( |
| { |
| name: 'PBKDF2', |
| salt: salt, |
| iterations: PBKDF2_ITERATIONS, |
| hash: 'SHA-256' |
| }, |
| keyMaterial, |
| { name: 'AES-GCM', length: 256 }, |
| true, |
| ['encrypt', 'decrypt'] |
| ); |
| } |
| |
| async function encrypt(text, key) { |
| const encoder = new TextEncoder(); |
| const data = encoder.encode(text); |
| const iv = crypto.getRandomValues(new Uint8Array(12)); |
| |
| const encryptedContent = await crypto.subtle.encrypt( |
| { name: 'AES-GCM', iv: iv }, |
| key, |
| data |
| ); |
| |
| const combined = new Uint8Array(iv.length + encryptedContent.byteLength); |
| combined.set(iv); |
| combined.set(new Uint8Array(encryptedContent), iv.length); |
| |
| return btoa(String.fromCharCode.apply(null, combined)); |
| } |
| |
| async function decrypt(encryptedBase64, key) { |
| try { |
| const combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0)); |
| const iv = combined.slice(0, 12); |
| const encryptedContent = combined.slice(12); |
| |
| const decrypted = await crypto.subtle.decrypt( |
| { name: 'AES-GCM', iv: iv }, |
| key, |
| encryptedContent |
| ); |
| |
| return new TextDecoder().decode(decrypted); |
| } catch (error) { |
| console.error('Decryption failed:', error); |
| throw new Error('Decryption failed. Check password.'); |
| } |
| } |
| |
| function base64ToUint8Array(base64) { |
| const binaryString = atob(base64); |
| const len = binaryString.length; |
| const bytes = new Uint8Array(len); |
| for (let i = 0; i < len; i++) { |
| bytes[i] = binaryString.charCodeAt(i); |
| } |
| return bytes; |
| } |
| |
| |
| document.getElementById('passwordInput').focus(); |
| document.getElementById('passwordInput').addEventListener('keypress', e => { |
| if (e.key === 'Enter') login(); |
| }); |
| |
| |
| function fixIOSViewport() { |
| const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; |
| if (isIOS) { |
| const vh = window.innerHeight * 0.01; |
| document.documentElement.style.setProperty('--vh', `${vh}px`); |
| |
| window.addEventListener('resize', () => { |
| const vh = window.innerHeight * 0.01; |
| document.documentElement.style.setProperty('--vh', `${vh}px`); |
| }); |
| } |
| } |
| |
| |
| fixIOSViewport(); |
| |
| async function login() { |
| if (isWorking) return; |
| isWorking = true; |
| |
| const password = document.getElementById('passwordInput').value; |
| const errorDiv = document.getElementById('loginError'); |
| const enterBtn = document.querySelector('.enter-btn'); |
| |
| errorDiv.textContent = ''; |
| if (password.length < 8) { |
| errorDiv.textContent = 'Password must be at least 8 characters'; |
| isWorking = false; |
| return; |
| } |
| |
| enterBtn.textContent = 'Loading...'; |
| enterBtn.disabled = true; |
| |
| currentPassword = password; |
| fileHash = await generateFilenameHash(password); |
| |
| try { |
| const response = await fetch('/api/load', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ hash: fileHash }) |
| }); |
| |
| const data = await response.json(); |
| |
| if (!response.ok) { |
| throw new Error(data.error || 'Login failed'); |
| } |
| |
| currentSalt = base64ToUint8Array(data.salt); |
| |
| let content = ''; |
| if (data.content) { |
| const key = await deriveKey(currentPassword, currentSalt); |
| content = await decrypt(data.content, key); |
| } |
| |
| document.getElementById('editor').value = content; |
| document.getElementById('loginScreen').style.display = 'none'; |
| document.getElementById('editorScreen').style.display = 'flex'; |
| setupAutoSave(); |
| updateSaveStatus('Ready'); |
| updateWordCount(); |
| |
| } catch (error) { |
| errorDiv.textContent = error.message.includes('Decryption') ? 'Invalid password' : 'Connection error'; |
| } finally { |
| isWorking = false; |
| enterBtn.textContent = 'Enter'; |
| enterBtn.disabled = false; |
| } |
| } |
| |
| function setupAutoSave() { |
| const editor = document.getElementById('editor'); |
| |
| editor.addEventListener('input', () => { |
| clearTimeout(saveTimeout); |
| updateSaveStatus('Typing...'); |
| updateWordCount(); |
| |
| saveTimeout = setTimeout(saveContent, 1500); |
| }); |
| } |
| |
| async function saveContent() { |
| if (isWorking) return; |
| isWorking = true; |
| |
| updateSaveStatus('Saving...'); |
| |
| const content = document.getElementById('editor').value; |
| |
| try { |
| const key = await deriveKey(currentPassword, currentSalt); |
| const encryptedContent = await encrypt(content, key); |
| |
| const response = await fetch('/api/save', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| hash: fileHash, |
| content: encryptedContent |
| }) |
| }); |
| |
| if (response.ok) { |
| updateSaveStatus('Saved'); |
| } else { |
| const data = await response.json(); |
| updateSaveStatus(`Error: ${data.error || 'Save failed'}`); |
| } |
| } catch (error) { |
| console.error('Save failed:', error); |
| updateSaveStatus('Save failed'); |
| } finally { |
| isWorking = false; |
| } |
| } |
| |
| function goToLogin() { |
| const statusDiv = document.getElementById('saveStatus'); |
| |
| if (!statusDiv.className.includes('saved')) { |
| if (!confirm('You have unsaved changes. Are you sure you want to leave?')) { |
| return; |
| } |
| } |
| |
| |
| currentPassword = ''; |
| currentSalt = null; |
| fileHash = ''; |
| clearTimeout(saveTimeout); |
| |
| |
| document.getElementById('editor').value = ''; |
| document.getElementById('passwordInput').value = ''; |
| document.getElementById('loginError').textContent = ''; |
| document.getElementById('editorScreen').style.display = 'none'; |
| document.getElementById('loginScreen').style.display = 'flex'; |
| document.getElementById('passwordInput').focus(); |
| } |
| |
| function updateSaveStatus(status) { |
| const statusDiv = document.getElementById('saveStatus'); |
| statusDiv.textContent = status; |
| |
| statusDiv.className = 'save-status'; |
| if (status.includes('Saving') || status.includes('Typing')) { |
| statusDiv.className += ' saving'; |
| } else if (status === 'Saved') { |
| statusDiv.className += ' saved'; |
| } |
| } |
| |
| |
| window.addEventListener('beforeunload', function (e) { |
| const statusDiv = document.getElementById('saveStatus'); |
| |
| if (!statusDiv.className.includes('saved')) { |
| e.preventDefault(); |
| e.returnValue = ''; |
| } |
| }); |
| </script> |
| </body> |
|
|
| </html> |