quickSnote

This commit is contained in:
2026-09-23 13:36:24 -07:00
parent 35c71a1e6d
commit 04a40840d1
14 changed files with 844 additions and 1 deletions

View File

@@ -194,11 +194,150 @@ function header() {
el("div", { class: "topbar-right" }, [
el("select", { id: "user-select", class: "user-select", onchange: onUserChange }),
el("button", { class: "btn primary", text: "+ New note", onclick: () => openNote(null) }),
el("button", { class: "btn ghost", text: "Help", onclick: showHelp }),
el("button", { class: "btn ghost", text: "Capture", onclick: showTools }),
themeSelector(),
]),
]);
}
function showHelp() {
state.view = "help";
root.innerHTML = "";
state.editor?.destroy(); state.editor = null;
const back = el("button", { class: "btn ghost", text: "← Back", onclick: showHome });
const wrap = el("div", { class: "editor-wrap" }, [
el("div", { class: "editor-topbar" }, [back]),
el("div", { class: "help-card" }, [
el("h1", { text: "Help" }),
el("h2", { text: "Keyboard shortcuts" }),
el("table", { class: "help-table" }, [
el("tr", {}, [el("th", { text: "Shortcut" }), el("th", { text: "Action" })]),
helpRow("n", "New note (from the home page)"),
helpRow("Ctrl/Cmd + S", "Save the current note"),
helpRow("Ctrl/Cmd + Shift + S", "Save and open a new note"),
]),
el("h2", { text: "Tags" }),
el("p", { html: 'Type <code>#tag</code> anywhere in your note to tag it. Tags are normalized to lowercase and shown in the sidebar, search, and note list.' }),
el("h2", { text: "Lists" }),
el("p", { html: 'Type <code>- </code> at the start of a line for a bullet list, or <code>1. </code> for a numbered list. You can also use the <b>• List</b> and <b>1. List</b> toolbar buttons.' }),
el("h2", { text: "Editing" }),
el("p", { html: 'Use the toolbar to format text with <b>bold</b>, <i>italic</i>, headings, quotes, code, and horizontal rules. The editor also accepts standard Markdown shortcuts as you type.' }),
el("h2", { text: "Notes & sharing" }),
el("p", { html: 'Notes are plain Markdown files. A note lives in a <i>space</i> (a user), chosen with the dropdown at the top of the editor. Changing the space moves the note to that user.' }),
el("p", { html: 'Use <b>Attach</b> to upload a file and insert a link to it. <b>History</b> shows every change, and <b>Delete</b> soft-deletes a note.' }),
]),
]);
root.appendChild(wrap);
}
function helpRow(keys, action) {
return el("tr", {}, [el("td", { html: "<code>" + keys + "</code>" }), el("td", { text: action })]);
}
function showTools() {
state.view = "tools";
root.innerHTML = "";
state.editor?.destroy(); state.editor = null;
const back = el("button", { class: "btn ghost", text: "← Back", onclick: showHome });
const extStatus = el("div", { class: "muted", text: "" });
const installBtn = el("button", { class: "btn primary", text: "Install Browser Extension" });
const qsStatus = el("span", { class: "muted", text: "" });
const qsApply = el("button", { class: "btn", text: "Apply to Extension" });
const card = el("div", { class: "help-card" }, [
el("h1", { text: "Quick capture" }),
el("p", { html: 'The <b>quickSnote</b> browser extension lets you jot a note from any page. It prefills the note with the text you\'ve selected, and a <b>Page</b> button inserts the current page\'s title and link.' }),
el("h2", { text: "Firefox" }),
el("div", { class: "tools-row" }, [installBtn, extStatus]),
el("h2", { text: "Quick settings" }),
el("p", { text: "Point the extension at this snotes server (" + window.location.origin + ")." }),
el("div", { class: "tools-row" }, [qsApply, qsStatus]),
el("h2", { text: "Manual install" }),
el("p", { text: "Open about:debugging#/runtime/this-firefox → Load Temporary Add-on… → select browser-extension/manifest.json." }),
el("h2", { text: "Using it" }),
el("p", { html: 'Open the toolbar icon, pick a <b>user</b>, type, and <b>Save</b> — or just stop typing for 8s and it autosaves.' }),
]);
const wrap = el("div", { class: "editor-wrap" }, [
el("div", { class: "editor-topbar" }, [back]),
card,
]);
root.appendChild(wrap);
// ── Extension detection (ping/pong) ──────────────────────────────────────
const xpiUrl = "/static/snotes-capture-firefox.xpi";
let detected = false;
let pingCount = 0;
let pingInterval = null;
let slowInterval = null;
function stopPing() {
if (pingInterval) clearInterval(pingInterval), pingInterval = null;
if (slowInterval) clearInterval(slowInterval), slowInterval = null;
}
function onDetected() {
detected = true;
stopPing();
extStatus.textContent = "✓ Extension detected";
installBtn.textContent = "Extension installed";
}
function doPing() {
window.postMessage({ kind: "snotes-ping" }, "*");
pingCount++;
if (pingCount >= 8) {
if (pingInterval) clearInterval(pingInterval), pingInterval = null;
if (!detected && !slowInterval) {
extStatus.textContent = "Extension not detected";
slowInterval = setInterval(() => window.postMessage({ kind: "snotes-ping" }, "*"), 2000);
}
}
}
installBtn.addEventListener("click", () => {
if (detected) return;
if (window.InstallTrigger) InstallTrigger.install({ "quickSnote": xpiUrl });
else window.location.href = xpiUrl;
extStatus.textContent = "Installing… check toolbar for prompt";
stopPing();
pingCount = 0;
pingInterval = setInterval(doPing, 500);
});
qsApply.addEventListener("click", () => {
window.postMessage({ kind: "snotes-settings", serverUrl: window.location.origin }, "*");
qsStatus.textContent = "Applying…";
setTimeout(() => { if (qsStatus.textContent === "Applying…") qsStatus.textContent = "No response — is the extension installed?"; }, 5000);
});
window.addEventListener("message", (event) => {
if (event.source !== window) return;
const msg = event.data;
if (msg && msg.kind === "snotes-pong") onDetected();
if (msg && msg.kind === "snotes-settings-ack") {
qsStatus.textContent = "✓ Applied";
setTimeout(() => { if (qsStatus.textContent === "✓ Applied") qsStatus.textContent = ""; }, 3000);
}
});
pingInterval = setInterval(doPing, 500);
doPing();
}
async function showHome() {
state.view = "home";
root.innerHTML = "";
@@ -341,6 +480,11 @@ function renderEditor(body, owner) {
]);
const editorArea = el("div", { class: "editor" });
const status = el("div", { class: "status", text: "" });
const hint = el("div", { class: "editor-hint" }, [
el("p", { html: 'Tags: type <code>#tag</code> anywhere in your note to add a tag.' }),
el("p", { html: 'Lists: type <code>- </code> for a bullet list or <code>1. </code> for a numbered list, or use the toolbar buttons.' }),
el("p", { html: 'Shortcuts: <code>Ctrl/Cmd+S</code> save · <code>Ctrl/Cmd+Shift+S</code> save &amp; new note.' }),
]);
const fileInput = el("input", { type: "file", style: "display:none" });
fileInput.addEventListener("change", () => uploadAttachment(fileInput));
@@ -350,6 +494,7 @@ function renderEditor(body, owner) {
el("div", { class: "editor-topbar" }, [back, spaceSel, el("div", { class: "spacer" }), attach, hist, del, save]),
toolbar,
editorArea,
hint,
status,
]);
wrap.appendChild(fileInput);
@@ -387,7 +532,7 @@ function runToolbar(action) {
cmds[action]?.();
}
async function saveNote() {
async function persistNote() {
const html = state.editor.getHTML();
const body = turndownService.turndown(html);
const status = document.querySelector(".status");
@@ -404,6 +549,17 @@ async function saveNote() {
} catch (e) { status.textContent = "Error: " + e.message; }
}
async function saveNote() {
const isNew = !state.noteId;
await persistNote();
if (isNew) showHome();
}
async function saveAndNewNote() {
await persistNote();
openNote(null);
}
async function deleteNote() {
if (!confirm("Delete this note?")) return;
await api("DELETE", "/api/notes/" + state.noteId);
@@ -451,4 +607,22 @@ async function showHistory() {
root.appendChild(overlay);
}
document.addEventListener("keydown", (e) => {
const mod = e.ctrlKey || e.metaKey;
if (state.view === "home") {
if (e.key === "n" && !mod) {
const active = document.activeElement;
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable)) return;
e.preventDefault();
openNote(null);
}
} else if (state.view === "editor") {
if (mod && e.key.toLowerCase() === "s") {
e.preventDefault();
if (e.shiftKey) saveAndNewNote();
else saveNote();
}
}
});
boot();