diff --git a/app.py b/app.py index a52638f..33b2c84 100644 --- a/app.py +++ b/app.py @@ -140,6 +140,7 @@ HTML = """ --text: #111; --border: #ddd; --btn-bg: #fff; + --btn-hover: #f0f0f0; --gutter-bg: #f8fafc; --gutter-border: #eee; --gutter-color: #9ca3af; @@ -151,6 +152,7 @@ HTML = """ --text: #F0F0F0; --border: #3a3b45; --btn-bg: #2a2b33; + --btn-hover: #35363f; --gutter-bg: #1e1f28; --gutter-border: #2e2f3a; --gutter-color: #6b7280; @@ -162,8 +164,9 @@ HTML = """ body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji"; max-width: 1000px; margin: 0 auto; padding: 1rem; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; } header { display:flex; justify-content:space-between; align-items:center; margin-bottom: .5rem; flex-shrink: 0; } - a,button { padding:.35rem .6rem; text-decoration:none; border:1px solid var(--border); border-radius:4px; background:var(--btn-bg); color:var(--text); cursor:pointer; } - #newpad { background:#000; color:#fff; border:1px solid #000; font-weight:bold; } + a,button { padding:.35rem 0; text-decoration:none; border:1px solid var(--border); border-radius:4px; background:var(--btn-bg); color:var(--text); cursor:pointer; font-size:.9rem; font-weight:bold; min-width:2rem; text-align:center; display:inline-block; } + a:hover,button:hover { background:var(--btn-hover); } + #lock-btn { font-weight:normal; } #status { font-size:.9rem; opacity:.7; margin-left:.5rem; } #status::before { content: "●"; margin-right: .3rem; color: #ef4444; } #status.connected::before { color: #22c55e; } @@ -175,8 +178,6 @@ HTML = """ user-select:none; min-width: 3ch; white-space: pre; height: 100%; overflow: hidden; } #t { padding:.5rem .75rem; width:100%; height: 100%; resize: none; border:0; outline:0; overflow:auto; white-space: pre; background:var(--bg); color:var(--text); } - #newpad { margin-left:.5rem; } - #info { margin-left:.5rem; font-size: 0.8rem; } pre { margin: 0; } /* Password protection */ #lock-btn.locked { background:#dbeafe; border-color:#3b82f6; } @@ -206,11 +207,11 @@ HTML = """ disconnected
- - - New pad - Info - + + + + + + β“˜
@@ -246,7 +247,7 @@ const rand = () => Math.random().toString(36).slice(2, 6); // 4 chars // Theme function applyTheme(dark) { document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); - $('#theme-btn').textContent = dark ? 'Light' : 'Dark'; + $('#theme-btn').textContent = dark ? 'β˜€' : '☾'; } function toggleTheme() { const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; @@ -273,6 +274,7 @@ $("#padname").textContent = "/"+docId+"/"; let ws, ver = 0, clientId = Math.random().toString(36).slice(2), debounce; let isProtected = false, isAuthed = false; +const urlPw = new URLSearchParams(location.search).get("pw") || ""; // --- Line numbers --- const ta = $("#t"); @@ -328,11 +330,11 @@ function removePassword() { function updateLockBtn() { const btn = $("#lock-btn"); if (isProtected) { - btn.textContent = "Locked"; + btn.textContent = "πŸ”’οΈŽ"; btn.classList.add("locked"); btn.title = "Password protected – click to manage"; } else { - btn.textContent = "Lock"; + btn.textContent = "πŸ”’οΈŽ"; btn.classList.remove("locked"); btn.title = "No password – click to set one"; } @@ -381,7 +383,11 @@ function connect(){ updateLockBtn(); if (msg.peers !== undefined) { const el = $("#peers"); el.textContent = msg.peers; el.style.display = "inline"; } if (isProtected && !isAuthed) { - showOverlay(); + if (urlPw) { + ws.send(JSON.stringify({type: "auth", password: urlPw})); + } else { + showOverlay(); + } } else { isAuthed = true; hideOverlay(); @@ -393,15 +399,17 @@ function connect(){ // Real init with content follows immediately from server } else if (msg.type === "error") { if (!isAuthed) { + showOverlay(); const errEl = $("#auth-error"); errEl.textContent = msg.message; errEl.style.display = "block"; } } else if (msg.type === "update" && msg.ver > ver && msg.clientId !== clientId) { const {selectionStart:s, selectionEnd:e} = ta; + const oldText = ta.value; ta.value = msg.text; ver = msg.ver; updateGutter(); - ta.selectionStart = Math.min(s, ta.value.length); - ta.selectionEnd = Math.min(e, ta.value.length); + ta.selectionStart = adjustCursor(oldText, msg.text, s); + ta.selectionEnd = adjustCursor(oldText, msg.text, e); } else if (msg.type === "peers_changed") { const el = $("#peers"); el.textContent = msg.count; @@ -426,7 +434,7 @@ async function copyToClipboard() { await navigator.clipboard.writeText(ta.value); const btn = $("#copy"); const original = btn.textContent; - btn.textContent = "Copied!"; + btn.textContent = "βœ“"; setTimeout(() => { btn.textContent = original; }, 1500); } catch (err) { // Fallback for older browsers @@ -434,11 +442,30 @@ async function copyToClipboard() { document.execCommand('copy'); const btn = $("#copy"); const original = btn.textContent; - btn.textContent = "Copied!"; + btn.textContent = "βœ“"; setTimeout(() => { btn.textContent = original; }, 1500); } } +// Adjust cursor position after a remote text update. +// Finds the single changed region (common prefix + suffix), +// then shifts the cursor accordingly: +// - change is after cursor β†’ no movement +// - change is before cursor β†’ shift by length delta +// - cursor was inside the changed region β†’ place at end of new content +function adjustCursor(oldText, newText, pos) { + let start = 0; + const minLen = Math.min(oldText.length, newText.length); + while (start < minLen && oldText[start] === newText[start]) start++; + if (pos <= start) return pos; // change is entirely after cursor + let oldEnd = oldText.length, newEnd = newText.length; + while (oldEnd > start && newEnd > start && oldText[oldEnd - 1] === newText[newEnd - 1]) { + oldEnd--; newEnd--; + } + if (pos >= oldEnd) return pos + (newEnd - oldEnd); // change is before cursor + return newEnd; // cursor was inside changed region +} + connect(); // Handle Tab key to insert 4 spaces instead of navigation