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} -
+