Archived and finished buckets, userscript nav chips #4
+29
-16
@@ -145,38 +145,51 @@ func (h *webHandler) index(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, http.StatusOK, "app", view)
|
||||
}
|
||||
|
||||
// filterBookmarks returns the subset keep reports true for, preserving order.
|
||||
// It always returns a non-nil slice so an empty tab renders its empty state.
|
||||
func filterBookmarks(all []Bookmark, keep func(Bookmark) bool) []Bookmark {
|
||||
out := []Bookmark{}
|
||||
for _, b := range all {
|
||||
if keep(b) {
|
||||
out = append(out, b)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildListView loads the list once and derives both the tab-filtered items and
|
||||
// the recent strip from it. The strip always reflects overall recency, not the
|
||||
// active tab, so it is built before filtering.
|
||||
// the recent strip from it.
|
||||
//
|
||||
// Archived and finished series appear in their own tab and nowhere else — not
|
||||
// in All, not in Updated, not in Favourites, and not in the recent strip. An
|
||||
// archived favourite therefore shows only under Archived: Favourites means
|
||||
// "favourites I am currently reading".
|
||||
func (h *webHandler) buildListView(tab string) (listView, error) {
|
||||
all, err := h.store.List() // already ordered updated_at DESC
|
||||
if err != nil {
|
||||
return listView{}, err
|
||||
}
|
||||
reading := filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusReading })
|
||||
|
||||
recent := all
|
||||
// The strip reflects overall reading recency, not the active tab.
|
||||
recent := reading
|
||||
if len(recent) > recentCount {
|
||||
recent = recent[:recentCount]
|
||||
}
|
||||
|
||||
items := all
|
||||
var items []Bookmark
|
||||
switch tab {
|
||||
case "fav":
|
||||
items = []Bookmark{}
|
||||
for _, b := range all {
|
||||
if b.Favorite {
|
||||
items = append(items, b)
|
||||
}
|
||||
}
|
||||
items = filterBookmarks(reading, func(b Bookmark) bool { return b.Favorite })
|
||||
case "new":
|
||||
items = []Bookmark{}
|
||||
for _, b := range all {
|
||||
if b.HasNewChapter() {
|
||||
items = append(items, b)
|
||||
}
|
||||
}
|
||||
items = filterBookmarks(reading, func(b Bookmark) bool { return b.HasNewChapter() })
|
||||
case "archived":
|
||||
items = filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusArchived })
|
||||
case "finished":
|
||||
items = filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusFinished })
|
||||
default:
|
||||
tab = "all"
|
||||
items = reading
|
||||
}
|
||||
return listView{Tab: tab, Recent: recent, Items: items}, nil
|
||||
}
|
||||
|
||||
@@ -513,3 +513,89 @@ func TestUIListNewTab(t *testing.T) {
|
||||
t.Fatal("new tab included a series already caught up")
|
||||
}
|
||||
}
|
||||
|
||||
// seedStatusRows puts one series in each bucket, the archived one also
|
||||
// favourited and with a new chapter out, so a leak into any reading-bucket tab
|
||||
// shows up as a failure rather than passing by accident.
|
||||
func seedStatusRows(t *testing.T, store *Store) {
|
||||
t.Helper()
|
||||
// floatPtr already exists in store_test.go — same package, reuse it.
|
||||
rows := []Bookmark{
|
||||
{Key: "asura:reading", Site: "asura", SeriesID: "reading", Title: "ReadingOne",
|
||||
Status: statusReading, LastChapterNum: 10, Favorite: true,
|
||||
LatestChapter: "11", LatestChapterNum: floatPtr(11)},
|
||||
{Key: "asura:archived", Site: "asura", SeriesID: "archived", Title: "ArchivedOne",
|
||||
Status: statusArchived, LastChapterNum: 5, Favorite: true,
|
||||
LatestChapter: "99", LatestChapterNum: floatPtr(99)},
|
||||
{Key: "asura:finished", Site: "asura", SeriesID: "finished", Title: "FinishedOne",
|
||||
Status: statusFinished, LastChapterNum: 200, Favorite: true},
|
||||
}
|
||||
for _, b := range rows {
|
||||
b.UpdatedAt = time.Now().UnixMilli()
|
||||
if _, err := store.Upsert(b); err != nil {
|
||||
t.Fatalf("seed %s: %v", b.Key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabsShowOnlyTheirBucket(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
seedStatusRows(t, store)
|
||||
|
||||
cases := []struct {
|
||||
tab string
|
||||
want, dontWant []string
|
||||
}{
|
||||
{"all", []string{"ReadingOne"}, []string{"ArchivedOne", "FinishedOne"}},
|
||||
{"new", []string{"ReadingOne"}, []string{"ArchivedOne", "FinishedOne"}},
|
||||
{"fav", []string{"ReadingOne"}, []string{"ArchivedOne", "FinishedOne"}},
|
||||
{"archived", []string{"ArchivedOne"}, []string{"ReadingOne", "FinishedOne"}},
|
||||
{"finished", []string{"FinishedOne"}, []string{"ReadingOne", "ArchivedOne"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.tab, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/ui/list?tab="+tc.tab, nil)
|
||||
req.AddCookie(sessionCookie(t, cfg))
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
body := rr.Body.String()
|
||||
for _, w := range tc.want {
|
||||
if !strings.Contains(body, w) {
|
||||
t.Fatalf("tab %s missing %s", tc.tab, w)
|
||||
}
|
||||
}
|
||||
for _, d := range tc.dontWant {
|
||||
if strings.Contains(body, d) {
|
||||
t.Fatalf("tab %s leaked %s", tc.tab, d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecentStripExcludesArchivedAndFinished(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)
|
||||
|
||||
strip := rr.Body.String()
|
||||
if i := strings.Index(strip, `id="list"`); i >= 0 {
|
||||
strip = strip[:i] // everything above the list is the recent section
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user