counter on feed select

This commit is contained in:
2026-07-13 17:46:49 +02:00
parent 671ed86b48
commit a898e7ac33
4 changed files with 30 additions and 5 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ import Modal from './modal/AddUrl.vue'
const router = useRouter()
const route = useRoute()
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feedFilter, feedTitles, setFeedFilter, unreadCount, lastProgrammaticScroll } = useFeeds()
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feedFilter, feedTitles, feedUnreadCounts, setFeedFilter, unreadCount, lastProgrammaticScroll } = useFeeds()
const headerRef = ref(null)
@@ -155,7 +155,7 @@ function handleToggleLayout() {
@change="setFeedFilter($event.target.value || null)"
>
<option value="">All feeds</option>
<option v-for="title in feedTitles" :key="title" :value="title">{{ title }}</option>
<option v-for="title in feedTitles" :key="title" :value="title">{{ title }} ({{ feedUnreadCounts[title] ?? 0 }})</option>
</select>
<button
class="app-nav__hamburger"
+3 -2
View File
@@ -241,8 +241,9 @@ describe('AppNav', () => {
await flushPromises()
const options = wrapper.find('.app-nav__filter').findAll('option').map(o => o.text())
// "All feeds" first, then distinct titles sorted alphabetically.
expect(options).toEqual(['All feeds', 'Feed A', 'Feed B'])
// "All feeds" first, then distinct titles sorted alphabetically, each with
// its unread count.
expect(options).toEqual(['All feeds', 'Feed A (2)', 'Feed B (1)'])
})
it('does not render the feed filter when there are no feeds', async () => {
+13 -1
View File
@@ -13,7 +13,7 @@ class FakeIntersectionObserver {
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
describe('useFeeds', () => {
const { feeds, allItems, feedFilter, feedTitles, unreadCount, setFeedFilter, showMessage, message, showModal, fetchData, sync, getReadable, setInitialLoad, handleIntersection, markAllRead, setupIntersectionObserver } = useFeeds()
const { feeds, allItems, feedFilter, feedTitles, feedUnreadCounts, unreadCount, setFeedFilter, showMessage, message, showModal, fetchData, sync, getReadable, setInitialLoad, handleIntersection, markAllRead, setupIntersectionObserver } = useFeeds()
beforeEach(() => {
localStorage.setItem('user-token', 'test-token')
@@ -252,6 +252,18 @@ describe('useFeeds', () => {
await setFeedFilter(null)
expect(unreadCount.value).toBe(3)
})
it('exposes per-feed unread counts, excluding items marked read in place', async () => {
axios.get.mockResolvedValueOnce(twoFeedsResponse)
await fetchData()
expect(feedUnreadCounts.value).toEqual({ 'Feed A': 2, 'Feed B': 1 })
// Article view marks items read in place (feed.read = true) without
// removing them from the master — the counts must not include those.
allItems.value.find(i => i.id === 3).read = true
expect(feedUnreadCounts.value).toEqual({ 'Feed A': 1, 'Feed B': 1 })
})
})
it('strips leftover embedded-video placeholder headings', async () => {
+12
View File
@@ -22,6 +22,17 @@ const feedTitles = computed(() => {
if (feedFilter.value) titles.add(feedFilter.value)
return [...titles].sort((a, b) => a.localeCompare(b))
})
// Unread count per feed title, shown next to each option in the filter
// <select>. Derived from the master (allItems) so article view's
// read-but-still-shown items are excluded. A title kept in feedTitles with no
// unread items left has no key here — render it as 0.
const feedUnreadCounts = computed(() => {
const counts = {}
for (const item of allItems.value) {
if (!item.read) counts[item.feedTitle] = (counts[item.feedTitle] ?? 0) + 1
}
return counts
})
// The header badge count: unread items in the selected feed when a filter is
// active, otherwise the global unread total. Derived from the master (allItems)
// rather than the displayed `feeds` so article view's read-but-still-shown
@@ -466,6 +477,7 @@ export function useFeeds() {
allItems,
feedFilter,
feedTitles,
feedUnreadCounts,
unreadCount,
setFeedFilter,
showMessage,