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 <noreply@anthropic.com>
This commit is contained in:
@@ -14,11 +14,14 @@
|
|||||||
if (!card.hidden) visible++;
|
if (!card.hidden) visible++;
|
||||||
});
|
});
|
||||||
|
|
||||||
// The strip is never filtered, so leaving it up during a search puts
|
// The strip is All-only and never filtered. It lives outside #list, so an
|
||||||
// non-matching covers above an empty list and the screen contradicts
|
// htmx tab switch does not re-render it and this is what takes it down;
|
||||||
// itself.
|
// leaving it up during a search would also put non-matching covers above
|
||||||
|
// an empty list.
|
||||||
var recent = document.querySelector(".recent");
|
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
|
// An empty bucket already explains itself server-side; this only speaks
|
||||||
// when the filter is what emptied the screen.
|
// 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.
|
// htmx replaces the list on a tab switch, so re-apply to the new cards.
|
||||||
document.body.addEventListener("htmx:afterSwap", applyFilter);
|
document.body.addEventListener("htmx:afterSwap", applyFilter);
|
||||||
|
document.addEventListener("mangabm:refilter", applyFilter);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function setActiveTab(el) {
|
function setActiveTab(el) {
|
||||||
@@ -52,6 +56,9 @@ function setActiveTab(el) {
|
|||||||
if (on) t.setAttribute("aria-current", "page");
|
if (on) t.setAttribute("aria-current", "page");
|
||||||
else t.removeAttribute("aria-current");
|
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
|
// The chapter-edit form and the archive/finish/remove confirm rows are the
|
||||||
|
|||||||
+9
-6
@@ -184,12 +184,6 @@ func (h *webHandler) buildListView(tab string) (listView, error) {
|
|||||||
}
|
}
|
||||||
reading := filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusReading })
|
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() })
|
withNew := filterBookmarks(reading, func(b Bookmark) bool { return b.HasNewChapter() })
|
||||||
|
|
||||||
var items []Bookmark
|
var items []Bookmark
|
||||||
@@ -206,6 +200,15 @@ func (h *webHandler) buildListView(tab string) (listView, error) {
|
|||||||
tab = "all"
|
tab = "all"
|
||||||
items = reading
|
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
|
return listView{Tab: tab, Recent: recent, Items: items, NewCount: len(withNew)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-10
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"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()
|
cfg := webConfig()
|
||||||
srv, store := newWebTestServer(t, cfg)
|
srv, store := newWebTestServer(t, cfg)
|
||||||
seedStatusRows(t, store)
|
seedStatusRows(t, store)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/?tab=archived", nil)
|
for _, tab := range []string{"all", "new", "fav", "archived", "finished"} {
|
||||||
req.AddCookie(sessionCookie(t, cfg))
|
if strings.Contains(stripOf(t, srv, cfg, tab), `class="recent"`) {
|
||||||
rr := httptest.NewRecorder()
|
t.Fatalf("tab %s rendered the strip for a library of one reading series", tab)
|
||||||
srv.ServeHTTP(rr, req)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strip := rr.Body.String()
|
// Past recentCount the strip stops repeating the whole list.
|
||||||
if i := strings.Index(strip, `id="list"`); i >= 0 {
|
for i := 0; i <= recentCount; i++ {
|
||||||
strip = strip[:i] // everything above the list is the recent section
|
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") {
|
if strings.Contains(strip, "ArchivedOne") || strings.Contains(strip, "FinishedOne") {
|
||||||
t.Fatal("recent strip included an archived or finished series")
|
t.Fatal("recent strip included an archived or finished series")
|
||||||
}
|
}
|
||||||
if !strings.Contains(strip, "ReadingOne") {
|
for _, tab := range []string{"new", "fav", "archived", "finished"} {
|
||||||
t.Fatal("recent strip dropped the reading series")
|
if strings.Contains(stripOf(t, srv, cfg, tab), `class="recent"`) {
|
||||||
|
t.Fatalf("tab %s rendered the strip", tab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user