feat: major platform expansion — Brain service, RSS reader, iOS app, AI assistants, Firefox extension
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>
This commit is contained in:
202
extensions/brain-firefox/popup.js
Normal file
202
extensions/brain-firefox/popup.js
Normal file
@@ -0,0 +1,202 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user