| | <!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> |