fix: mobile nav full height + upload paste box, PDF zoom fit
- Mobile nav sheet: flex layout, bottom section pushed to bottom - Upload paste box + file picker added to mobile nav drawer - Date shown at bottom of mobile nav - PDF viewer: added #zoom=page-fit to iframe URL Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,22 @@
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav-bottom">
|
||||
<div class="rail-upload-row">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="paste-box" contenteditable="true" onpaste={onPasteBox} role="textbox" tabindex="0">
|
||||
{#if uploadStatus === 'uploading'}Uploading...{:else if uploadStatus === 'done'}Saved!{:else}Paste screenshot{/if}
|
||||
</div>
|
||||
<button class="upload-icon-btn" onclick={() => uploadInput?.click()} title="Browse files">
|
||||
<Upload size={13} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="rail-date">
|
||||
<CalendarDays size={14} strokeWidth={1.8} />
|
||||
<span>{new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())}</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{/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;
|
||||
|
||||
@@ -583,7 +583,7 @@
|
||||
{@const pdfAsset = selectedItem.assets?.find(a => a.asset_type === 'original_upload')}
|
||||
{#if pdfAsset}
|
||||
<iframe
|
||||
src="/api/brain/storage/{selectedItem.id}/original_upload/{pdfAsset.filename}"
|
||||
src="/api/brain/storage/{selectedItem.id}/original_upload/{pdfAsset.filename}#zoom=page-fit"
|
||||
title={selectedItem.title || 'PDF'}
|
||||
class="viewer-iframe"
|
||||
></iframe>
|
||||
|
||||
@@ -27,13 +27,18 @@
|
||||
let autoScrollSpeed = $state(1.5);
|
||||
let articleListEl: HTMLDivElement;
|
||||
let scrollRAF: number | null = null;
|
||||
let scrollInterval: ReturnType<typeof setInterval> | 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<string, number> = {};
|
||||
let stagedAutoReadIds = new Set<number>();
|
||||
|
||||
// ── Helpers ──
|
||||
function timeAgo(dateStr: string): string {
|
||||
@@ -309,51 +314,77 @@
|
||||
|
||||
// ── 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);
|
||||
checkScrolledCards();
|
||||
if (nextY >= maxY - 1) {
|
||||
stopAutoScroll();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!articleListEl) return;
|
||||
articleListEl.scrollTop += delta;
|
||||
const maxScroll = articleListEl.scrollHeight - articleListEl.clientHeight;
|
||||
if (!lastAutoCheckTs || timestamp - lastAutoCheckTs > 220) {
|
||||
lastAutoCheckTs = timestamp;
|
||||
checkScrolledCards();
|
||||
}
|
||||
if (articleListEl.scrollTop >= maxScroll - 1) {
|
||||
stopAutoScroll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
scrollRAF = requestAnimationFrame(step);
|
||||
}
|
||||
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,20 +455,49 @@
|
||||
if (!id) return;
|
||||
const article = articles.find(a => a.id === id);
|
||||
if (article && !article.read) {
|
||||
if (autoScrollActive) {
|
||||
if (!stagedAutoReadIds.has(id)) {
|
||||
stagedAutoReadIds.add(id);
|
||||
newlyRead++;
|
||||
}
|
||||
} else {
|
||||
article.read = true;
|
||||
pendingReadIds.push(id);
|
||||
newlyRead++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (newlyRead > 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function flushPendingReads() {
|
||||
if (!pendingReadIds.length) return;
|
||||
@@ -459,6 +519,8 @@ onMount(() => {
|
||||
return () => {
|
||||
if (flushTimer) clearTimeout(flushTimer);
|
||||
if (scrollCheckTimer) clearTimeout(scrollCheckTimer);
|
||||
if (scrollInterval) clearInterval(scrollInterval);
|
||||
commitStagedAutoReads();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -536,7 +598,7 @@ onMount(() => {
|
||||
{/if}
|
||||
|
||||
<!-- Middle Panel: Article List -->
|
||||
<div class="reader-list">
|
||||
<div class="reader-list" class:auto-scrolling={autoScrollActive}>
|
||||
<div class="list-header">
|
||||
<div class="list-header-top">
|
||||
<button class="mobile-menu" onclick={() => sidebarOpen = !sidebarOpen} aria-label="Toggle sidebar">
|
||||
@@ -594,7 +656,7 @@ onMount(() => {
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="article-list" bind:this={articleListEl} ontouchstart={handleScrollInterrupt} onwheel={handleScrollInterrupt} onscroll={handleListScroll}>
|
||||
<div class="article-list" class:auto-scrolling={autoScrollActive} bind:this={articleListEl} ontouchstart={handleScrollInterrupt} onwheel={handleScrollInterrupt} onscroll={handleListScroll}>
|
||||
{#each filteredArticles as article, index (article.id)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
@@ -1102,6 +1164,7 @@ onMount(() => {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245, 237, 227, 0.96) 0%, rgba(239, 230, 219, 0.94) 100%);
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
.list-header {
|
||||
padding: 14px 14px 10px;
|
||||
@@ -1113,6 +1176,30 @@ onMount(() => {
|
||||
.list-header-top {
|
||||
gap: 12px;
|
||||
}
|
||||
.reader-list.auto-scrolling {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 18;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245, 237, 227, 0.98) 0%, rgba(239, 230, 219, 0.98) 100%);
|
||||
}
|
||||
.reader-list.auto-scrolling .list-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 28;
|
||||
}
|
||||
.article-list.auto-scrolling {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-top: 6px;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 18px);
|
||||
}
|
||||
.list-view-name { font-size: 1.44rem; }
|
||||
.list-subtitle { font-size: 0.84rem; line-height: 1.4; }
|
||||
.article-list {
|
||||
|
||||
Reference in New Issue
Block a user