keep state of aritcle/list view

This commit is contained in:
2026-07-12 17:22:30 +02:00
parent 31991ea1f8
commit 2b1d1c57ff
2 changed files with 62 additions and 7 deletions
+28 -6
View File
@@ -34,12 +34,21 @@ const unreadCount = computed(() => {
})
const message = ref('')
const showModal = ref(false)
const viewMode = ref('list') // 'list' | 'article' — toggled from the hamburger menu
const viewMode = ref(localStorage.getItem('viewMode') || 'list') // 'list' | 'article' — toggled from the hamburger menu, persisted per device
const currentIndex = ref(0)
const layout = ref(localStorage.getItem('layout') || 'list') // 'list' | 'cards' — list-view display style, toggled from the hamburger menu
let observer; // Declare observer outside the setup function
let initialLoad = false
// An IntersectionObserver always delivers an initial snapshot for every element
// it starts observing. If the observer (re)connects while the page is scrolled
// down — e.g. leaving a scrolled article view, an HMR module reload, or the
// post-splice re-setup in handleIntersection — that snapshot reports every
// article above the viewport as "not intersecting / scrolled past" and would
// mark them all read at once. So each setupIntersectionObserver() sets this and
// handleIntersection() drops exactly the first callback after a connect; only
// genuine scroll-driven exits mark read after that.
let skipNextObservation = false
// Timestamp (performance.now()) of the most recent programmatic scroll / list
// mutation that moves the page without user intent — currently the list-view
@@ -282,14 +291,25 @@ function setupIntersectionObserver() {
});
const observedDivs = document.querySelectorAll(".observe");
if (observedDivs.length > 0) {
observedDivs.forEach(observedDiv => {
observer.observe(observedDiv);
})
}
// Arm the skip only when we actually observe nodes: an observe() batch fires
// an initial snapshot to drop (a connect while scrolled down would otherwise
// mass-mark everything above the viewport), but with nothing observed there's
// no snapshot — and assigning unconditionally clears any stale flag from a
// prior no-op setup (e.g. one run in article view, which has no .observe nodes).
skipNextObservation = observedDivs.length > 0
observedDivs.forEach(observedDiv => {
observer.observe(observedDiv);
})
}
function handleIntersection(entries, topbarHeight = 0) {
// Drop the initial snapshot fired on (re)connect — see skipNextObservation.
// The old observer is disconnected before this new one observes, so only the
// latest observer's initial callback reaches here; one skip is enough.
if (skipNextObservation) {
skipNextObservation = false
return
}
// Resolve all affected feeds before touching feeds.value — the target.id
// indices are render-time positions that shift once we splice the array.
const readFeeds = entries
@@ -386,6 +406,7 @@ async function leaveArticleView() {
allItems.value = allItems.value.filter(feed => !feed.read)
currentIndex.value = 0
viewMode.value = 'list'
localStorage.setItem('viewMode', viewMode.value)
// The v-if on the list container tears down and recreates all .observe DOM
// nodes when switching views, so the intersection observer must be
// re-pointed at the new elements after Vue has finished rendering.
@@ -405,6 +426,7 @@ function toggleViewMode() {
observer = null
}
viewMode.value = 'article'
localStorage.setItem('viewMode', viewMode.value)
currentIndex.value = 0
markCurrentArticleRead()
}