diff --git a/.gitignore b/.gitignore index 8367d6b..e0f1c3f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,28 @@ gateway/data/ # Test artifacts test-results/ + +# Brain storage (user data, not code) +services/brain/storage/ +services/brain/data/ + +# Reader data +services/reader/data/ + +# Screenshots (uploaded images, not code) +screenshots/*.png +screenshots/*.jpg +screenshots/*.jpeg + +# iOS build artifacts +ios/Platform/Platform.xcodeproj/xcuserdata/ +ios/Platform/Platform.xcodeproj/project.xcworkspace/xcuserdata/ +ios/Platform.bak/ + +# Node modules +**/node_modules/ + +# Temp files +*.pyc +__pycache__/ +.DS_Store diff --git a/docker-compose.yml b/docker-compose.yml index d7f8641..31214b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,8 @@ services: - IMMICH_API_KEY=${IMMICH_API_KEY} - KARAKEEP_URL=${KARAKEEP_URL:-http://192.168.1.42:3005} - KARAKEEP_API_KEY=${KARAKEEP_API_KEY} + - OPENAI_API_KEY=${OPENAI_API_KEY} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-5.2} - BODY_SIZE_LIMIT=52428800 - TZ=${TZ:-America/Chicago} networks: @@ -64,6 +66,7 @@ services: - TASKS_BACKEND_URL=http://tasks-service:8098 - TASKS_SERVICE_API_KEY=${TASKS_SERVICE_API_KEY} - BRAIN_BACKEND_URL=http://brain-api:8200 + - READER_BACKEND_URL=http://reader-api:8300 - QBITTORRENT_HOST=${QBITTORRENT_HOST:-192.168.1.42} - QBITTORRENT_PORT=${QBITTORRENT_PORT:-8080} - QBITTORRENT_USERNAME=${QBITTORRENT_USERNAME:-admin} diff --git a/extensions/brain-firefox/background.js b/extensions/brain-firefox/background.js new file mode 100644 index 0000000..6d2e60b --- /dev/null +++ b/extensions/brain-firefox/background.js @@ -0,0 +1,250 @@ +"use strict"; + +var API_BASE = "https://dash.quadjourney.com"; + +console.log("[Brain] Background script loaded"); + +// ── Auth helpers ── + +function getSession() { + return browser.storage.local.get("brainSession").then(function(data) { + return data.brainSession || null; + }); +} + +function apiRequest(path, opts) { + return getSession().then(function(session) { + if (!session) throw new Error("Not logged in"); + + opts = opts || {}; + opts.headers = opts.headers || {}; + opts.headers["Cookie"] = "platform_session=" + session; + opts.credentials = "include"; + + return fetch(API_BASE + path, opts).then(function(resp) { + if (resp.status === 401 || resp.status === 403) { + browser.storage.local.remove("brainSession"); + throw new Error("Session expired"); + } + if (!resp.ok) throw new Error("API error: " + resp.status); + return resp.json(); + }); + }); +} + +// ── Save functions ── + +function saveLink(url, title, note) { + var body = { type: "link", url: url }; + if (title) body.title = title; + if (note && note.trim()) body.raw_content = note.trim(); + return apiRequest("/api/brain/items", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +function saveNote(text) { + return apiRequest("/api/brain/items", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "note", raw_content: text }), + }); +} + +function saveImage(imageUrl) { + return fetch(imageUrl).then(function(resp) { + if (!resp.ok) throw new Error("Failed to download image"); + return resp.blob(); + }).then(function(blob) { + var parts = imageUrl.split("/"); + var filename = (parts[parts.length - 1] || "image.jpg").split("?")[0]; + var formData = new FormData(); + formData.append("file", blob, filename); + + return getSession().then(function(session) { + if (!session) throw new Error("Not logged in"); + return fetch(API_BASE + "/api/brain/items/upload", { + method: "POST", + headers: { "Cookie": "platform_session=" + session }, + credentials: "include", + body: formData, + }); + }); + }).then(function(resp) { + if (!resp.ok) throw new Error("Upload failed: " + resp.status); + return resp.json(); + }); +} + +// ── Badge helpers ── + +function showBadge(tabId, text, color, duration) { + browser.action.setBadgeText({ text: text, tabId: tabId }); + browser.action.setBadgeBackgroundColor({ color: color, tabId: tabId }); + setTimeout(function() { + browser.action.setBadgeText({ text: "", tabId: tabId }); + }, duration || 2000); +} + +// ── Context menus ── + +browser.runtime.onInstalled.addListener(function() { + console.log("[Brain] Creating context menus"); + browser.contextMenus.create({ + id: "save-page", + title: "Save page to Brain", + contexts: ["page"], + }); + browser.contextMenus.create({ + id: "save-link", + title: "Save link to Brain", + contexts: ["link"], + }); + browser.contextMenus.create({ + id: "save-image", + title: "Save image to Brain", + contexts: ["image"], + }); +}); + +browser.contextMenus.onClicked.addListener(function(info, tab) { + getSession().then(function(session) { + if (!session) { + browser.action.openPopup(); + return; + } + + var promise; + if (info.menuItemId === "save-page") { + promise = saveLink(tab.url, tab.title); + } else if (info.menuItemId === "save-link") { + promise = saveLink(info.linkUrl, info.linkText); + } else if (info.menuItemId === "save-image") { + promise = saveImage(info.srcUrl); + } + + if (promise) { + promise.then(function() { + showBadge(tab.id, "OK", "#059669", 2000); + }).catch(function(e) { + showBadge(tab.id, "ERR", "#DC2626", 3000); + console.error("[Brain] Save failed:", e); + }); + } + }); +}); + +// ── Keyboard shortcut ── + +browser.commands.onCommand.addListener(function(command) { + if (command === "save-page") { + browser.tabs.query({ active: true, currentWindow: true }).then(function(tabs) { + var tab = tabs[0]; + if (!tab || !tab.url) return; + + getSession().then(function(session) { + if (!session) { + browser.action.openPopup(); + return; + } + saveLink(tab.url, tab.title).then(function() { + showBadge(tab.id, "OK", "#059669", 2000); + }).catch(function(e) { + showBadge(tab.id, "ERR", "#DC2626", 3000); + }); + }); + }); + } +}); + +// ── Message handler (from popup) ── + +browser.runtime.onMessage.addListener(function(msg, sender, sendResponse) { + console.log("[Brain] Got message:", msg.action); + + if (msg.action === "login") { + fetch(API_BASE + "/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: msg.username, + password: msg.password, + }), + credentials: "include", + }).then(function(resp) { + if (!resp.ok) { + sendResponse({ success: false, error: "Invalid credentials" }); + return; + } + // Wait for cookie to be set + return new Promise(function(r) { setTimeout(r, 300); }).then(function() { + return browser.cookies.get({ + url: API_BASE, + name: "platform_session", + }); + }).then(function(cookie) { + if (cookie && cookie.value) { + return browser.storage.local.set({ brainSession: cookie.value }).then(function() { + sendResponse({ success: true }); + }); + } + sendResponse({ success: false, error: "Login OK but could not capture session" }); + }); + }).catch(function(e) { + console.error("[Brain] Login error:", e); + sendResponse({ success: false, error: e.message || "Connection failed" }); + }); + return true; + } + + if (msg.action === "logout") { + browser.storage.local.remove("brainSession").then(function() { + sendResponse({ success: true }); + }); + return true; + } + + if (msg.action === "check-auth") { + getSession().then(function(session) { + if (!session) { + sendResponse({ authenticated: false }); + return; + } + return apiRequest("/api/auth/me").then(function(data) { + sendResponse({ authenticated: true, user: data.user || data }); + }).catch(function() { + sendResponse({ authenticated: false }); + }); + }); + return true; + } + + if (msg.action === "save-link") { + saveLink(msg.url, msg.title, msg.note).then(function(result) { + sendResponse(result); + }).catch(function(e) { + sendResponse({ error: e.message }); + }); + return true; + } + + if (msg.action === "save-note") { + saveNote(msg.text).then(function(result) { + sendResponse(result); + }).catch(function(e) { + sendResponse({ error: e.message }); + }); + return true; + } + + if (msg.action === "save-image") { + saveImage(msg.url).then(function(result) { + sendResponse(result); + }).catch(function(e) { + sendResponse({ error: e.message }); + }); + return true; + } +}); diff --git a/extensions/brain-firefox/brain-firefox.xpi b/extensions/brain-firefox/brain-firefox.xpi new file mode 100644 index 0000000..55c190a Binary files /dev/null and b/extensions/brain-firefox/brain-firefox.xpi differ diff --git a/extensions/brain-firefox/manifest.json b/extensions/brain-firefox/manifest.json new file mode 100644 index 0000000..88373cc --- /dev/null +++ b/extensions/brain-firefox/manifest.json @@ -0,0 +1,49 @@ +{ + "manifest_version": 3, + "name": "Brain - Save to Second Brain", + "version": "1.0.0", + "description": "One-click save pages, notes, and images to your Second Brain", + "permissions": [ + "activeTab", + "contextMenus", + "storage", + "cookies" + ], + "host_permissions": [ + "https://dash.quadjourney.com/*" + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/brain-16.png", + "32": "icons/brain-32.png", + "48": "icons/brain-48.png" + } + }, + "background": { + "scripts": ["background.js"] + }, + "commands": { + "save-page": { + "suggested_key": { + "default": "Alt+Shift+S" + }, + "description": "Save current page to Brain" + } + }, + "icons": { + "16": "icons/brain-16.png", + "32": "icons/brain-32.png", + "48": "icons/brain-48.png", + "128": "icons/brain-128.png" + }, + "browser_specific_settings": { + "gecko": { + "id": "brain@quadjourney.com", + "data_collection_permissions": { + "required": ["none"], + "optional": [] + } + } + } +} diff --git a/extensions/brain-firefox/popup.html b/extensions/brain-firefox/popup.html new file mode 100644 index 0000000..a838675 --- /dev/null +++ b/extensions/brain-firefox/popup.html @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + diff --git a/extensions/brain-firefox/popup.js b/extensions/brain-firefox/popup.js new file mode 100644 index 0000000..caa8ccd --- /dev/null +++ b/extensions/brain-firefox/popup.js @@ -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(); +}); diff --git a/extensions/brain-firefox/test-popup.html b/extensions/brain-firefox/test-popup.html new file mode 100644 index 0000000..7adb8ed --- /dev/null +++ b/extensions/brain-firefox/test-popup.html @@ -0,0 +1,8 @@ + + + + +

Brain Extension

+

If you see this, the popup works.

+ + diff --git a/frontend-v2/src/app.css b/frontend-v2/src/app.css index da5f984..ad33435 100644 --- a/frontend-v2/src/app.css +++ b/frontend-v2/src/app.css @@ -12,8 +12,9 @@ ═══════════════════════════════════════════════ */ @layer base { - html, body { overflow-x: hidden; } - body { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); } + html { background-color: #f5efe6; } + body { overflow-x: clip; } + body { padding-bottom: env(safe-area-inset-bottom); } :root { /* ── Fonts ── */ --font: 'Outfit', -apple-system, system-ui, sans-serif; @@ -106,7 +107,7 @@ /* ── LIGHT MODE — Zinc + Emerald ── */ :root { - --canvas: #FAFAFA; + --canvas: #f5efe6; --surface: #FFFFFF; --surface-secondary: #F4F4F5; --card: #FFFFFF; diff --git a/frontend-v2/src/app.html b/frontend-v2/src/app.html index bd524fe..2ee60cb 100644 --- a/frontend-v2/src/app.html +++ b/frontend-v2/src/app.html @@ -3,6 +3,9 @@ + + + diff --git a/frontend-v2/src/lib/components/assistant/BrainAssistantDrawer.svelte b/frontend-v2/src/lib/components/assistant/BrainAssistantDrawer.svelte new file mode 100644 index 0000000..779df73 --- /dev/null +++ b/frontend-v2/src/lib/components/assistant/BrainAssistantDrawer.svelte @@ -0,0 +1,480 @@ + + +{#if open} + + + +{/if} + + diff --git a/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte b/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte index a6a117b..c46a281 100644 --- a/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte +++ b/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte @@ -1,16 +1,26 @@ {#if open} - {/each} - {#if drafts.length > 1} + {#if activeDrafts().length > 1}
@@ -298,12 +341,12 @@
Ready to add
-
{drafts.length} items
+
{activeDrafts().length} items
-
{drafts[0]?.meal_type || 'meal'}
+
{activeDrafts()[0]?.meal_type || 'meal'}
- {#each drafts as item} + {#each activeDrafts() as item}
{item.food_name}
{Math.round(item.calories || 0)} cal
@@ -311,7 +354,7 @@ {/each}
- {drafts.reduce((sum, item) => sum + Math.round(item.calories || 0), 0)} cal total + {activeDrafts().reduce((sum, item) => sum + Math.round(item.calories || 0), 0)} cal total
- {:else if draft?.food_name} + {:else if activeDraft()?.food_name}
@@ -332,19 +375,19 @@
Ready to add
-
{draft.food_name}
+
{activeDraft()?.food_name}
- {#if draft.meal_type} -
{draft.meal_type}
+ {#if activeDraft()?.meal_type} +
{activeDraft()?.meal_type}
{/if}
- {Math.round(draft.calories || 0)} cal - {Math.round(draft.protein || 0)}p - {Math.round(draft.carbs || 0)}c - {Math.round(draft.fat || 0)}f - {Math.round(draft.sugar || 0)} sugar - {Math.round(draft.fiber || 0)} fiber + {Math.round(activeDraft()?.calories || 0)} cal + {Math.round(activeDraft()?.protein || 0)}p + {Math.round(activeDraft()?.carbs || 0)}c + {Math.round(activeDraft()?.fat || 0)}f + {Math.round(activeDraft()?.sugar || 0)} sugar + {Math.round(activeDraft()?.fiber || 0)} fiber
@@ -410,7 +453,7 @@ class="compose-input" bind:value={input} bind:this={composerEl} - placeholder="Add 2 boiled eggs for breakfast..." + placeholder={allowBrain ? 'Log food, save a note, ask Brain...' : 'Log food or ask about calories...'} onkeydown={handleKeydown} type="text" /> @@ -537,9 +580,12 @@ .assistant-body { min-height: 0; overflow: hidden; + display: flex; + flex-direction: column; } .thread { + flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; @@ -549,6 +595,7 @@ display: flex; flex-direction: column; gap: 10px; + scrollbar-gutter: stable; } .bundle-card { @@ -642,6 +689,30 @@ color: #f8f4ee; } + .source-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .source-chip { + display: grid; + gap: 2px; + padding: 8px 10px; + border-radius: 14px; + text-decoration: none; + background: rgba(255, 255, 255, 0.76); + border: 1px solid rgba(36, 26, 18, 0.08); + color: #241c14; + } + + .source-chip small { + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #7a6a5b; + } + .image-bubble { width: min(240px, 100%); padding: 8px; diff --git a/frontend-v2/src/lib/components/layout/AppShell.svelte b/frontend-v2/src/lib/components/layout/AppShell.svelte index 704256d..a0bacca 100644 --- a/frontend-v2/src/lib/components/layout/AppShell.svelte +++ b/frontend-v2/src/lib/components/layout/AppShell.svelte @@ -12,6 +12,7 @@ LibraryBig, Menu, Package2, + MessageSquare, Search, Settings2, SquareCheckBig, @@ -193,8 +194,9 @@
ops workspace
- @@ -220,7 +222,8 @@ {/each} -
+ +
e.preventDefault()} ondrop={onRailDrop}>
@@ -230,10 +233,6 @@
-
- - {new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())} -
{/if} @@ -250,13 +249,14 @@ radial-gradient(circle at top left, rgba(214, 120, 58, 0.08), transparent 22%), radial-gradient(circle at 85% 10%, rgba(65, 91, 82, 0.12), transparent 20%), linear-gradient(180deg, #f5efe6 0%, #efe6da 48%, #ede7de 100%); + background-color: #f5efe6; } .shell { --shell-ink: #1e1812; --shell-muted: #6b6256; --shell-line: rgba(35, 26, 17, 0.11); - min-height: 100vh; + min-height: 100dvh; display: grid; grid-template-columns: 270px minmax(0, 1fr); position: relative; @@ -525,10 +525,17 @@ } .command-trigger.mobile { - width: 40px; height: 40px; justify-content: center; - padding: 0; + padding: 0 12px; + gap: 6px; + font-weight: 700; + color: var(--shell-ink); + } + + .command-trigger.mobile span { + font-size: 0.78rem; + letter-spacing: 0.02em; } .mobile-menu-btn { @@ -557,33 +564,15 @@ top: 0; left: 0; bottom: 0; - width: min(300px, 82vw); - padding: 16px 14px; - padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + width: min(320px, 86vw); + padding: 18px 16px; background: linear-gradient(180deg, rgba(250, 246, 239, 0.96), rgba(244, 237, 228, 0.94)); border-right: 1px solid var(--shell-line); backdrop-filter: blur(20px); z-index: 40; - display: flex; - flex-direction: column; - gap: 12px; - overflow-y: auto; - } - .mobile-nav-list { - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; - } - .mobile-nav-list a { - padding: 10px 12px; - } - .mobile-nav-bottom { display: grid; - gap: 8px; - padding-top: 12px; - border-top: 1px solid var(--shell-line); - flex-shrink: 0; + align-content: start; + gap: 18px; } .mobile-nav-head { @@ -634,5 +623,11 @@ .mobile-menu-btn { display: inline-flex; } + + .mobile-nav-upload { + margin-top: auto; + padding-top: 16px; + border-top: 1px solid var(--shell-line); + } } diff --git a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte index 35756a8..1acc50d 100644 --- a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte +++ b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte @@ -1,8 +1,23 @@ @@ -566,7 +589,32 @@
-
Feeds
+
+ Feeds +
+ + +
+
+ + {#if showFeedManager} +
+ { if (e.key === 'Enter') addFeed(); }} /> + +
+ {/if} + {#each feedCategories as cat, i}
+ {#if showFeedManager} + + {/if} - +
{/each}
{/if} @@ -598,7 +656,7 @@ {/if} -
+
{article.timeAgo} @@ -713,6 +771,11 @@ {#if filteredArticles.length === 0}
No articles to show
{/if} + {#if hasMore && !loading} + + {/if}
@@ -737,11 +800,11 @@
-