Brain Service: - Playwright stealth crawler replacing browserless (og:image, Readability, Reddit JSON API) - AI classification with tag definitions and folder assignment - YouTube video download via yt-dlp - Karakeep migration complete (96 items) - Taxonomy management (folders with icons/colors, tags) - Discovery shuffle, sort options, search (Meilisearch + pgvector) - Item tag/folder editing, card color accents RSS Reader Service: - Custom FastAPI reader replacing Miniflux - Feed management (add/delete/refresh), category support - Full article extraction via Readability - Background content fetching for new entries - Mark all read with confirmation - Infinite scroll, retention cleanup (30/60 day) - 17 feeds migrated from Miniflux iOS App (SwiftUI): - Native iOS 17+ app with @Observable architecture - Cookie-based auth, configurable gateway URL - Dashboard with custom background photo + frosted glass widgets - Full fitness module (today/templates/goals/food library) - AI assistant chat (fitness + brain, raw JSON state management) - 120fps ProMotion support AI Assistants (Gateway): - Unified dispatcher with fitness/brain domain detection - Fitness: natural language food logging, photo analysis, multi-item splitting - Brain: save/append/update/delete notes, search & answer, undo support - Madiha user gets fitness-only (brain disabled) Firefox Extension: - One-click save to Brain from any page - Login with platform credentials - Right-click context menu (save page/link/image) - Notes field for URL saves - Signed and published on AMO Other: - Reader bookmark button routes to Brain (was Karakeep) - Fitness food library with "Add" button + add-to-meal popup - Kindle send file size check (25MB SMTP2GO limit) - Atelier UI as default (useAtelierShell=true) - Mobile upload box in nav drawer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
203 lines
5.5 KiB
JavaScript
203 lines
5.5 KiB
JavaScript
const loginView = document.getElementById("login-view");
|
|
const saveView = document.getElementById("save-view");
|
|
const successView = document.getElementById("success-view");
|
|
|
|
const loginBtn = document.getElementById("login-btn");
|
|
const loginUser = document.getElementById("login-user");
|
|
const loginPass = document.getElementById("login-pass");
|
|
const loginError = document.getElementById("login-error");
|
|
const logoutBtn = document.getElementById("logout-btn");
|
|
const userName = document.getElementById("user-name");
|
|
|
|
const tabLink = document.getElementById("tab-link");
|
|
const tabNote = document.getElementById("tab-note");
|
|
const linkMode = document.getElementById("link-mode");
|
|
const noteMode = document.getElementById("note-mode");
|
|
|
|
const pageTitle = document.getElementById("page-title");
|
|
const pageUrl = document.getElementById("page-url");
|
|
const noteToggleBtn = document.getElementById("note-toggle-btn");
|
|
const linkNote = document.getElementById("link-note");
|
|
const saveLinkBtn = document.getElementById("save-link-btn");
|
|
|
|
const noteText = document.getElementById("note-text");
|
|
const saveNoteBtn = document.getElementById("save-note-btn");
|
|
const saveError = document.getElementById("save-error");
|
|
|
|
let currentTab = null;
|
|
|
|
// ── View management ──
|
|
|
|
function showView(view) {
|
|
loginView.classList.add("hidden");
|
|
saveView.classList.add("hidden");
|
|
successView.classList.add("hidden");
|
|
view.classList.remove("hidden");
|
|
}
|
|
|
|
function showError(el, msg) {
|
|
el.textContent = msg;
|
|
el.classList.remove("hidden");
|
|
}
|
|
function hideError(el) {
|
|
el.classList.add("hidden");
|
|
}
|
|
|
|
// ── Init ──
|
|
|
|
async function init() {
|
|
const resp = await browser.runtime.sendMessage({ action: "check-auth" });
|
|
if (resp.authenticated) {
|
|
userName.textContent = resp.user?.display_name || resp.user?.username || "";
|
|
showView(saveView);
|
|
await loadCurrentTab();
|
|
} else {
|
|
showView(loginView);
|
|
loginUser.focus();
|
|
}
|
|
}
|
|
|
|
async function loadCurrentTab() {
|
|
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
|
if (tab) {
|
|
currentTab = tab;
|
|
pageTitle.textContent = tab.title || "Untitled";
|
|
try {
|
|
pageUrl.textContent = new URL(tab.url).hostname;
|
|
} catch {
|
|
pageUrl.textContent = tab.url;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Login ──
|
|
|
|
loginBtn.addEventListener("click", async () => {
|
|
const user = loginUser.value.trim();
|
|
const pass = loginPass.value;
|
|
if (!user || !pass) return;
|
|
|
|
loginBtn.disabled = true;
|
|
loginBtn.textContent = "Signing in...";
|
|
hideError(loginError);
|
|
|
|
try {
|
|
const resp = await browser.runtime.sendMessage({
|
|
action: "login",
|
|
username: user,
|
|
password: pass,
|
|
});
|
|
|
|
if (resp && resp.success) {
|
|
const auth = await browser.runtime.sendMessage({ action: "check-auth" });
|
|
userName.textContent = auth.user?.display_name || auth.user?.username || "";
|
|
showView(saveView);
|
|
await loadCurrentTab();
|
|
} else {
|
|
showError(loginError, (resp && resp.error) || "Login failed");
|
|
}
|
|
} catch (e) {
|
|
showError(loginError, "Error: " + (e.message || e));
|
|
}
|
|
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = "Sign in";
|
|
});
|
|
|
|
loginPass.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") loginBtn.click();
|
|
});
|
|
|
|
// ── Logout ──
|
|
|
|
logoutBtn.addEventListener("click", async () => {
|
|
await browser.runtime.sendMessage({ action: "logout" });
|
|
showView(loginView);
|
|
loginUser.value = "";
|
|
loginPass.value = "";
|
|
});
|
|
|
|
// ── Mode tabs ──
|
|
|
|
tabLink.addEventListener("click", () => {
|
|
tabLink.classList.add("active");
|
|
tabNote.classList.remove("active");
|
|
linkMode.classList.remove("hidden");
|
|
noteMode.classList.add("hidden");
|
|
hideError(saveError);
|
|
});
|
|
|
|
tabNote.addEventListener("click", () => {
|
|
tabNote.classList.add("active");
|
|
tabLink.classList.remove("active");
|
|
noteMode.classList.remove("hidden");
|
|
linkMode.classList.add("hidden");
|
|
hideError(saveError);
|
|
noteText.focus();
|
|
});
|
|
|
|
// ── Note toggle on link mode ──
|
|
|
|
noteToggleBtn.addEventListener("click", () => {
|
|
linkNote.classList.toggle("hidden");
|
|
if (!linkNote.classList.contains("hidden")) {
|
|
noteToggleBtn.textContent = "- Hide note";
|
|
linkNote.focus();
|
|
} else {
|
|
noteToggleBtn.textContent = "+ Add a note";
|
|
}
|
|
});
|
|
|
|
// ── Save link ──
|
|
|
|
saveLinkBtn.addEventListener("click", async () => {
|
|
if (!currentTab?.url) return;
|
|
saveLinkBtn.disabled = true;
|
|
saveLinkBtn.textContent = "Saving...";
|
|
hideError(saveError);
|
|
|
|
try {
|
|
await browser.runtime.sendMessage({
|
|
action: "save-link",
|
|
url: currentTab.url,
|
|
title: currentTab.title,
|
|
note: linkNote.value.trim() || undefined,
|
|
});
|
|
|
|
showView(successView);
|
|
setTimeout(() => window.close(), 1500);
|
|
} catch (e) {
|
|
showError(saveError, e.message || "Save failed");
|
|
saveLinkBtn.disabled = false;
|
|
saveLinkBtn.textContent = "Save page";
|
|
}
|
|
});
|
|
|
|
// ── Save note ──
|
|
|
|
saveNoteBtn.addEventListener("click", async () => {
|
|
const text = noteText.value.trim();
|
|
if (!text) return;
|
|
saveNoteBtn.disabled = true;
|
|
saveNoteBtn.textContent = "Saving...";
|
|
hideError(saveError);
|
|
|
|
try {
|
|
await browser.runtime.sendMessage({ action: "save-note", text });
|
|
showView(successView);
|
|
setTimeout(() => window.close(), 1500);
|
|
} catch (e) {
|
|
showError(saveError, e.message || "Save failed");
|
|
saveNoteBtn.disabled = false;
|
|
saveNoteBtn.textContent = "Save note";
|
|
}
|
|
});
|
|
|
|
// ── Start ──
|
|
init().catch((e) => {
|
|
console.error("[Brain popup] Init failed:", e);
|
|
// Show login view as fallback
|
|
showView(loginView);
|
|
loginUser.focus();
|
|
});
|