quickSnote
This commit is contained in:
176
static/app.js
176
static/app.js
@@ -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 & 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();
|
||||
|
||||
Reference in New Issue
Block a user