Download index.html from jebin2/Paper: direct link, hf CLI and curl.
- Browser
- Download file 32.2 kB
-
https://huggingface.co/spaces/jebin2/Paper/resolve/main/index.html
- Command line
-
hf download hf://spaces/jebin2/Paper/index.html
-
curl -L -o index.html https://huggingface.co/spaces/jebin2/Paper/resolve/main/index.html
32.2 kB
| <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 { | |
| /* --- Colorful & Playful DARK --- */ | |
| --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 */ | |
| .login-screen { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100%; | |
| padding: 20px; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| /* Flip Card Container */ | |
| .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 & Card Back */ | |
| .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 scrollbar */ | |
| .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 */ | |
| .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; | |
| } | |
| /* Scrollbar styling */ | |
| .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); | |
| } | |
| /* Animation for mode switching */ | |
| .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); | |
| } | |
| } | |
| /* Focus improvements */ | |
| .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); | |
| } | |
| } | |
| /* Texture overlay for paper feel */ | |
| .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); | |
| } | |
| /* Mobile Responsive */ | |
| @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"> | |
| <!-- Front: Login --> | |
| <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> | |
| <!-- Back: Info --> | |
| <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; | |
| // --- CARD FLIP --- | |
| function flipCard() { | |
| const container = document.getElementById('cardContainer'); | |
| container.classList.toggle('flipped'); | |
| } | |
| // --- WORD COUNT --- | |
| 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`; | |
| } | |
| // --- CRYPTOGRAPHY (unchanged) --- | |
| 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; | |
| } | |
| // --- APPLICATION LOGIC --- | |
| document.getElementById('passwordInput').focus(); | |
| document.getElementById('passwordInput').addEventListener('keypress', e => { | |
| if (e.key === 'Enter') login(); | |
| }); | |
| // iOS Safari viewport fix | |
| 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`); | |
| }); | |
| } | |
| } | |
| // Call on page load | |
| 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'); | |
| // Check if there are unsaved changes | |
| if (!statusDiv.className.includes('saved')) { | |
| if (!confirm('You have unsaved changes. Are you sure you want to leave?')) { | |
| return; | |
| } | |
| } | |
| // Clear sensitive data | |
| currentPassword = ''; | |
| currentSalt = null; | |
| fileHash = ''; | |
| clearTimeout(saveTimeout); | |
| // Reset UI | |
| 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'; | |
| } | |
| } | |
| // Prevent accidental page close | |
| window.addEventListener('beforeunload', function (e) { | |
| const statusDiv = document.getElementById('saveStatus'); | |
| // Check if there are unsaved changes | |
| if (!statusDiv.className.includes('saved')) { | |
| e.preventDefault(); | |
| e.returnValue = ''; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |