From c560a61a9abac1f62949373aab14604087989b3b Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Thu, 30 Jul 2026 19:10:42 +0700 Subject: [PATCH] Show the recent strip only where it is not a duplicate The strip is the head of the same updated_at DESC reading list the All tab renders, and app.html put it on every tab: on a small library it repeated the list immediately below it while eating ~230px of the first phone screen, and on Archived and Finished it showed reading series above the opposite bucket. buildListView now fills Recent only for tab=all with more reading series than the strip holds. Because the strip lives outside the htmx-swapped #list, an in-page tab switch cannot re-render it, so applyFilter also decides its visibility from the active tab; setActiveTab pokes it after a swap. Co-Authored-By: Claude Opus 5 --- backend/static/filter.js | 15 ++++++++--- backend/web.go | 15 ++++++----- backend/web_test.go | 54 ++++++++++++++++++++++++++++++++-------- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/backend/static/filter.js b/backend/static/filter.js index 64d9d49..33bc404 100644 --- a/backend/static/filter.js +++ b/backend/static/filter.js @@ -14,11 +14,14 @@ if (!card.hidden) visible++; }); - // The strip is never filtered, so leaving it up during a search puts - // non-matching covers above an empty list and the screen contradicts - // itself. + // The strip is All-only and never filtered. It lives outside #list, so an + // htmx tab switch does not re-render it and this is what takes it down; + // leaving it up during a search would also put non-matching covers above + // an empty list. var recent = document.querySelector(".recent"); - if (recent) recent.hidden = needle !== ""; + if (recent) { + recent.hidden = needle !== "" || !document.querySelector('.tabs a.active[href="/?tab=all"]'); + } // An empty bucket already explains itself server-side; this only speaks // when the filter is what emptied the screen. @@ -43,6 +46,7 @@ // htmx replaces the list on a tab switch, so re-apply to the new cards. document.body.addEventListener("htmx:afterSwap", applyFilter); + document.addEventListener("mangabm:refilter", applyFilter); })(); function setActiveTab(el) { @@ -52,6 +56,9 @@ function setActiveTab(el) { if (on) t.setAttribute("aria-current", "page"); else t.removeAttribute("aria-current"); }); + // The strip is outside the swapped region, so its visibility is re-decided + // here rather than by the server that just answered. + document.dispatchEvent(new Event("mangabm:refilter")); } // The chapter-edit form and the archive/finish/remove confirm rows are the diff --git a/backend/web.go b/backend/web.go index 62c7dee..83ed317 100644 --- a/backend/web.go +++ b/backend/web.go @@ -184,12 +184,6 @@ func (h *webHandler) buildListView(tab string) (listView, error) { } reading := filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusReading }) - // The strip reflects overall reading recency, not the active tab. - recent := reading - if len(recent) > recentCount { - recent = recent[:recentCount] - } - withNew := filterBookmarks(reading, func(b Bookmark) bool { return b.HasNewChapter() }) var items []Bookmark @@ -206,6 +200,15 @@ func (h *webHandler) buildListView(tab string) (listView, error) { tab = "all" items = reading } + // The strip is the head of the same updated_at DESC reading list the All tab + // renders, so it only earns its ~230px of the first phone screen where it is + // not a pure duplicate of what sits right below it: on All, with more series + // than it shows. On the other tabs it either repeats the list or, worse, + // puts reading series above the archived and finished buckets. + var recent []Bookmark + if tab == "all" && len(reading) > recentCount { + recent = reading[:recentCount] + } return listView{Tab: tab, Recent: recent, Items: items, NewCount: len(withNew)}, nil } diff --git a/backend/web_test.go b/backend/web_test.go index edbf541..f7988e6 100644 --- a/backend/web_test.go +++ b/backend/web_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/http" "net/http/httptest" "net/url" @@ -578,25 +579,58 @@ func TestTabsShowOnlyTheirBucket(t *testing.T) { } } -func TestRecentStripExcludesArchivedAndFinished(t *testing.T) { +// stripOf returns everything above the list, which is where the recent section +// renders. +func stripOf(t *testing.T, srv http.Handler, cfg Config, tab string) string { + t.Helper() + req := httptest.NewRequest(http.MethodGet, "/?tab="+tab, nil) + req.AddCookie(sessionCookie(t, cfg)) + rr := httptest.NewRecorder() + srv.ServeHTTP(rr, req) + body := rr.Body.String() + if i := strings.Index(body, `id="list"`); i >= 0 { + body = body[:i] + } + return body +} + +// The strip is the head of the same list the All tab renders, so it only shows +// where it is not a pure duplicate: on All, with more reading series than it +// holds. +func TestRecentStripOnlyOnAllAndOnlyWhenItAddsSomething(t *testing.T) { cfg := webConfig() srv, store := newWebTestServer(t, cfg) seedStatusRows(t, store) - req := httptest.NewRequest(http.MethodGet, "/?tab=archived", nil) - req.AddCookie(sessionCookie(t, cfg)) - rr := httptest.NewRecorder() - srv.ServeHTTP(rr, req) + for _, tab := range []string{"all", "new", "fav", "archived", "finished"} { + if strings.Contains(stripOf(t, srv, cfg, tab), `class="recent"`) { + t.Fatalf("tab %s rendered the strip for a library of one reading series", tab) + } + } - strip := rr.Body.String() - if i := strings.Index(strip, `id="list"`); i >= 0 { - strip = strip[:i] // everything above the list is the recent section + // Past recentCount the strip stops repeating the whole list. + for i := 0; i <= recentCount; i++ { + b := Bookmark{ + Key: fmt.Sprintf("asura:more%d", i), Site: "asura", + SeriesID: fmt.Sprintf("more%d", i), Title: fmt.Sprintf("MoreReading%d", i), + Status: statusReading, LastChapterNum: 1, UpdatedAt: time.Now().UnixMilli(), + } + if _, err := store.Upsert(b); err != nil { + t.Fatalf("seed %s: %v", b.Key, err) + } + } + + strip := stripOf(t, srv, cfg, "all") + if !strings.Contains(strip, `class="recent"`) { + t.Fatal("strip missing on All with more reading series than it shows") } if strings.Contains(strip, "ArchivedOne") || strings.Contains(strip, "FinishedOne") { t.Fatal("recent strip included an archived or finished series") } - if !strings.Contains(strip, "ReadingOne") { - t.Fatal("recent strip dropped the reading series") + for _, tab := range []string{"new", "fav", "archived", "finished"} { + if strings.Contains(stripOf(t, srv, cfg, tab), `class="recent"`) { + t.Fatalf("tab %s rendered the strip", tab) + } } }