Compare commits

..

3 commits
v0.5.0 ... main

59
app.py
View file

@ -140,6 +140,7 @@ HTML = """<!doctype html>
--text: #111; --text: #111;
--border: #ddd; --border: #ddd;
--btn-bg: #fff; --btn-bg: #fff;
--btn-hover: #f0f0f0;
--gutter-bg: #f8fafc; --gutter-bg: #f8fafc;
--gutter-border: #eee; --gutter-border: #eee;
--gutter-color: #9ca3af; --gutter-color: #9ca3af;
@ -151,6 +152,7 @@ HTML = """<!doctype html>
--text: #F0F0F0; --text: #F0F0F0;
--border: #3a3b45; --border: #3a3b45;
--btn-bg: #2a2b33; --btn-bg: #2a2b33;
--btn-hover: #35363f;
--gutter-bg: #1e1f28; --gutter-bg: #1e1f28;
--gutter-border: #2e2f3a; --gutter-border: #2e2f3a;
--gutter-color: #6b7280; --gutter-color: #6b7280;
@ -162,8 +164,9 @@ HTML = """<!doctype 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"; 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; } 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; } 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; } 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; }
#newpad { background:#000; color:#fff; border:1px solid #000; font-weight:bold; } a:hover,button:hover { background:var(--btn-hover); }
#lock-btn { font-weight:normal; }
#status { font-size:.9rem; opacity:.7; margin-left:.5rem; } #status { font-size:.9rem; opacity:.7; margin-left:.5rem; }
#status::before { content: "●"; margin-right: .3rem; color: #ef4444; } #status::before { content: "●"; margin-right: .3rem; color: #ef4444; }
#status.connected::before { color: #22c55e; } #status.connected::before { color: #22c55e; }
@ -175,8 +178,6 @@ HTML = """<!doctype html>
user-select:none; min-width: 3ch; white-space: pre; height: 100%; overflow: hidden; } 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; #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); } 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; } pre { margin: 0; }
/* Password protection */ /* Password protection */
#lock-btn.locked { background:#dbeafe; border-color:#3b82f6; } #lock-btn.locked { background:#dbeafe; border-color:#3b82f6; }
@ -206,11 +207,11 @@ HTML = """<!doctype html>
<strong id="padname"></strong><span id="peers"></span><span id="status">disconnected</span> <strong id="padname"></strong><span id="peers"></span><span id="status">disconnected</span>
</div> </div>
<div style="position:relative; display:flex; align-items:center; gap:.25rem;"> <div style="position:relative; display:flex; align-items:center; gap:.25rem;">
<button id="copy" onclick="copyToClipboard()">Copy</button> <button id="copy" onclick="copyToClipboard()" title="Copy to clipboard"></button>
<button id="lock-btn" onclick="togglePwPanel()" title="No password click to set one">Lock</button> <a id="newpad" href="/" target="_blank" title="Opens a new pad in a new tab">+</a>
<a id="newpad" href="/" target="_blank">New pad</a> <button id="lock-btn" onclick="togglePwPanel()" title="No password click to set one">🔒</button>
<a id="info" href="/system/info">Info</a> <button id="theme-btn" onclick="toggleTheme()" title="Toggle dark/light mode"></button>
<button id="theme-btn" onclick="toggleTheme()" title="Toggle dark/light mode">Dark</button> <a id="info" href="/system/info" title="System info"></a>
<div id="pw-panel"> <div id="pw-panel">
<label>Password protection</label> <label>Password protection</label>
<input id="pw-input" type="password" placeholder="New password…" onkeydown="if(event.key==='Enter')setPassword()"/> <input id="pw-input" type="password" placeholder="New password…" onkeydown="if(event.key==='Enter')setPassword()"/>
@ -246,7 +247,7 @@ const rand = () => Math.random().toString(36).slice(2, 6); // 4 chars
// Theme // Theme
function applyTheme(dark) { function applyTheme(dark) {
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
$('#theme-btn').textContent = dark ? 'Light' : 'Dark'; $('#theme-btn').textContent = dark ? '' : '';
} }
function toggleTheme() { function toggleTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 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 ws, ver = 0, clientId = Math.random().toString(36).slice(2), debounce;
let isProtected = false, isAuthed = false; let isProtected = false, isAuthed = false;
const urlPw = new URLSearchParams(location.search).get("pw") || "";
// --- Line numbers --- // --- Line numbers ---
const ta = $("#t"); const ta = $("#t");
@ -328,11 +330,11 @@ function removePassword() {
function updateLockBtn() { function updateLockBtn() {
const btn = $("#lock-btn"); const btn = $("#lock-btn");
if (isProtected) { if (isProtected) {
btn.textContent = "Locked"; btn.textContent = "🔒︎";
btn.classList.add("locked"); btn.classList.add("locked");
btn.title = "Password protected click to manage"; btn.title = "Password protected click to manage";
} else { } else {
btn.textContent = "Lock"; btn.textContent = "🔒︎";
btn.classList.remove("locked"); btn.classList.remove("locked");
btn.title = "No password click to set one"; btn.title = "No password click to set one";
} }
@ -381,7 +383,11 @@ function connect(){
updateLockBtn(); updateLockBtn();
if (msg.peers !== undefined) { const el = $("#peers"); el.textContent = msg.peers; el.style.display = "inline"; } if (msg.peers !== undefined) { const el = $("#peers"); el.textContent = msg.peers; el.style.display = "inline"; }
if (isProtected && !isAuthed) { if (isProtected && !isAuthed) {
if (urlPw) {
ws.send(JSON.stringify({type: "auth", password: urlPw}));
} else {
showOverlay(); showOverlay();
}
} else { } else {
isAuthed = true; isAuthed = true;
hideOverlay(); hideOverlay();
@ -393,15 +399,17 @@ function connect(){
// Real init with content follows immediately from server // Real init with content follows immediately from server
} else if (msg.type === "error") { } else if (msg.type === "error") {
if (!isAuthed) { if (!isAuthed) {
showOverlay();
const errEl = $("#auth-error"); const errEl = $("#auth-error");
errEl.textContent = msg.message; errEl.textContent = msg.message;
errEl.style.display = "block"; errEl.style.display = "block";
} }
} else if (msg.type === "update" && msg.ver > ver && msg.clientId !== clientId) { } else if (msg.type === "update" && msg.ver > ver && msg.clientId !== clientId) {
const {selectionStart:s, selectionEnd:e} = ta; const {selectionStart:s, selectionEnd:e} = ta;
const oldText = ta.value;
ta.value = msg.text; ver = msg.ver; updateGutter(); ta.value = msg.text; ver = msg.ver; updateGutter();
ta.selectionStart = Math.min(s, ta.value.length); ta.selectionStart = adjustCursor(oldText, msg.text, s);
ta.selectionEnd = Math.min(e, ta.value.length); ta.selectionEnd = adjustCursor(oldText, msg.text, e);
} else if (msg.type === "peers_changed") { } else if (msg.type === "peers_changed") {
const el = $("#peers"); const el = $("#peers");
el.textContent = msg.count; el.textContent = msg.count;
@ -426,7 +434,7 @@ async function copyToClipboard() {
await navigator.clipboard.writeText(ta.value); await navigator.clipboard.writeText(ta.value);
const btn = $("#copy"); const btn = $("#copy");
const original = btn.textContent; const original = btn.textContent;
btn.textContent = "Copied!"; btn.textContent = "";
setTimeout(() => { btn.textContent = original; }, 1500); setTimeout(() => { btn.textContent = original; }, 1500);
} catch (err) { } catch (err) {
// Fallback for older browsers // Fallback for older browsers
@ -434,11 +442,30 @@ async function copyToClipboard() {
document.execCommand('copy'); document.execCommand('copy');
const btn = $("#copy"); const btn = $("#copy");
const original = btn.textContent; const original = btn.textContent;
btn.textContent = "Copied!"; btn.textContent = "";
setTimeout(() => { btn.textContent = original; }, 1500); 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(); connect();
// Handle Tab key to insert 4 spaces instead of navigation // Handle Tab key to insert 4 spaces instead of navigation