diff --git a/frontend-v2/src/lib/components/layout/AppShell.svelte b/frontend-v2/src/lib/components/layout/AppShell.svelte
index c8b2787..b2cd979 100644
--- a/frontend-v2/src/lib/components/layout/AppShell.svelte
+++ b/frontend-v2/src/lib/components/layout/AppShell.svelte
@@ -219,6 +219,22 @@
{/each}
+
+
+
+
+
+ {#if uploadStatus === 'uploading'}Uploading...{:else if uploadStatus === 'done'}Saved!{:else}Paste screenshot{/if}
+
+
+
+
+
+ {new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())}
+
+
{/if}
@@ -547,10 +563,17 @@
border-right: 1px solid var(--shell-line);
backdrop-filter: blur(20px);
z-index: 40;
- display: grid;
- align-content: start;
+ display: flex;
+ flex-direction: column;
gap: 18px;
}
+ .mobile-nav-list { flex: 1; }
+ .mobile-nav-bottom {
+ display: grid;
+ gap: 10px;
+ padding-top: 14px;
+ border-top: 1px solid var(--shell-line);
+ }
.mobile-nav-head {
display: flex;
diff --git a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte
index 8e1703f..445626b 100644
--- a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte
+++ b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte
@@ -583,7 +583,7 @@
{@const pdfAsset = selectedItem.assets?.find(a => a.asset_type === 'original_upload')}
{#if pdfAsset}
diff --git a/frontend-v2/src/lib/pages/reader/AtelierReaderPage.svelte b/frontend-v2/src/lib/pages/reader/AtelierReaderPage.svelte
index 2f097ce..a7a997b 100644
--- a/frontend-v2/src/lib/pages/reader/AtelierReaderPage.svelte
+++ b/frontend-v2/src/lib/pages/reader/AtelierReaderPage.svelte
@@ -27,13 +27,18 @@
let autoScrollSpeed = $state(1.5);
let articleListEl: HTMLDivElement;
let scrollRAF: number | null = null;
+ let scrollInterval: ReturnType | null = null;
let lastScrollTs = 0;
+ let scrollCarry = 0;
+ let lastAutoCheckTs = 0;
+ let mobileAutoStartScrollY = 0;
let loading = $state(true);
let loadingMore = $state(false);
let hasMore = $state(true);
let totalUnread = $state(0);
const LIMIT = 50;
let feedCounters: Record = {};
+ let stagedAutoReadIds = new Set();
// ── Helpers ──
function timeAgo(dateStr: string): string {
@@ -309,41 +314,55 @@
// ── Auto-scroll (requestAnimationFrame for smoothness) ──
function usesPageScroll(): boolean {
- return window.matchMedia('(max-width: 1024px)').matches;
+ return window.matchMedia('(max-width: 1024px)').matches && !autoScrollActive;
}
function startAutoScroll() {
if (scrollRAF) cancelAnimationFrame(scrollRAF);
+ if (scrollInterval) clearInterval(scrollInterval);
if (!usesPageScroll() && !articleListEl) return;
autoScrollActive = true;
lastScrollTs = 0;
+ scrollCarry = 0;
+ lastAutoCheckTs = 0;
+ if (usesPageScroll()) {
+ mobileAutoStartScrollY = window.scrollY;
+ requestAnimationFrame(() => {
+ articleListEl?.scrollTo({ top: mobileAutoStartScrollY, behavior: 'auto' });
+ });
+ scrollInterval = setInterval(() => {
+ if (!autoScrollActive) return;
+ if (!articleListEl) return;
+ const delta = Math.max(1, Math.round(4 * autoScrollSpeed));
+ const nextY = articleListEl.scrollTop + delta;
+ const maxY = Math.max(0, articleListEl.scrollHeight - articleListEl.clientHeight);
+ articleListEl.scrollTop = Math.min(nextY, maxY);
+ checkScrolledCards();
+ if (nextY >= maxY - 1) {
+ stopAutoScroll();
+ }
+ }, 40);
+ return;
+ }
function step(timestamp: number) {
if (!autoScrollActive) return;
const dt = lastScrollTs ? Math.min(34, timestamp - lastScrollTs) : 16;
lastScrollTs = timestamp;
- const pxPerSecond = 28 * autoScrollSpeed;
- const delta = pxPerSecond * (dt / 1000);
+ const pxPerSecond = 72 * autoScrollSpeed;
+ scrollCarry += pxPerSecond * (dt / 1000);
+ const delta = Math.max(1, Math.round(scrollCarry));
+ scrollCarry -= delta;
- if (usesPageScroll()) {
- const scroller = document.scrollingElement;
- if (!scroller) return;
- const nextY = scroller.scrollTop + delta;
- const maxY = Math.max(0, scroller.scrollHeight - window.innerHeight);
- scroller.scrollTop = Math.min(nextY, maxY);
+ if (!articleListEl) return;
+ articleListEl.scrollTop += delta;
+ const maxScroll = articleListEl.scrollHeight - articleListEl.clientHeight;
+ if (!lastAutoCheckTs || timestamp - lastAutoCheckTs > 220) {
+ lastAutoCheckTs = timestamp;
checkScrolledCards();
- if (nextY >= maxY - 1) {
- stopAutoScroll();
- return;
- }
- } else {
- if (!articleListEl) return;
- articleListEl.scrollTop += delta;
- const maxScroll = articleListEl.scrollHeight - articleListEl.clientHeight;
- checkScrolledCards();
- if (articleListEl.scrollTop >= maxScroll - 1) {
- stopAutoScroll();
- return;
- }
+ }
+ if (articleListEl.scrollTop >= maxScroll - 1) {
+ stopAutoScroll();
+ return;
}
scrollRAF = requestAnimationFrame(step);
@@ -351,9 +370,21 @@
scrollRAF = requestAnimationFrame(step);
}
function stopAutoScroll() {
+ const restorePageScroll = window.matchMedia('(max-width: 1024px)').matches && articleListEl
+ ? articleListEl.scrollTop
+ : null;
autoScrollActive = false;
lastScrollTs = 0;
+ scrollCarry = 0;
+ lastAutoCheckTs = 0;
if (scrollRAF) { cancelAnimationFrame(scrollRAF); scrollRAF = null; }
+ if (scrollInterval) { clearInterval(scrollInterval); scrollInterval = null; }
+ commitStagedAutoReads();
+ if (restorePageScroll !== null) {
+ requestAnimationFrame(() => {
+ window.scrollTo({ top: restorePageScroll, behavior: 'auto' });
+ });
+ }
}
function toggleAutoScroll() {
if (autoScrollActive) stopAutoScroll();
@@ -424,16 +455,45 @@
if (!id) return;
const article = articles.find(a => a.id === id);
if (article && !article.read) {
- article.read = true;
- pendingReadIds.push(id);
- newlyRead++;
+ if (autoScrollActive) {
+ if (!stagedAutoReadIds.has(id)) {
+ stagedAutoReadIds.add(id);
+ newlyRead++;
+ }
+ } else {
+ article.read = true;
+ pendingReadIds.push(id);
+ newlyRead++;
+ }
}
}
});
if (newlyRead > 0) {
- articles = [...articles];
+ if (!autoScrollActive) {
+ articles = [...articles];
+ decrementUnread(newlyRead);
+ if (flushTimer) clearTimeout(flushTimer);
+ flushTimer = setTimeout(flushPendingReads, 1000);
+ }
+ }
+ }
+
+ function commitStagedAutoReads() {
+ if (!stagedAutoReadIds.size) return;
+ const ids = [...stagedAutoReadIds];
+ stagedAutoReadIds = new Set();
+ let newlyRead = 0;
+ articles = articles.map((article) => {
+ if (ids.includes(article.id) && !article.read) {
+ newlyRead++;
+ return { ...article, read: true };
+ }
+ return article;
+ });
+ if (newlyRead > 0) {
decrementUnread(newlyRead);
+ pendingReadIds.push(...ids);
if (flushTimer) clearTimeout(flushTimer);
flushTimer = setTimeout(flushPendingReads, 1000);
}
@@ -453,12 +513,14 @@
}
// ── Init ──
-onMount(() => {
+ onMount(() => {
loadSidebar();
loadEntries();
return () => {
if (flushTimer) clearTimeout(flushTimer);
if (scrollCheckTimer) clearTimeout(scrollCheckTimer);
+ if (scrollInterval) clearInterval(scrollInterval);
+ commitStagedAutoReads();
};
});
@@ -536,7 +598,7 @@ onMount(() => {
{/if}
-