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)
|
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
|
// 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
|
// the recent strip from it.
|
||||||
// active tab, so it is built before filtering.
|
//
|
||||||
|
// 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) {
|
func (h *webHandler) buildListView(tab string) (listView, error) {
|
||||||
all, err := h.store.List() // already ordered updated_at DESC
|
all, err := h.store.List() // already ordered updated_at DESC
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return listView{}, err
|
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 {
|
if len(recent) > recentCount {
|
||||||
recent = recent[:recentCount]
|
recent = recent[:recentCount]
|
||||||
}
|
}
|
||||||
|
|
||||||
items := all
|
var items []Bookmark
|
||||||
switch tab {
|
switch tab {
|
||||||
case "fav":
|
case "fav":
|
||||||
items = []Bookmark{}
|
items = filterBookmarks(reading, func(b Bookmark) bool { return b.Favorite })
|
||||||
for _, b := range all {
|
|
||||||
if b.Favorite {
|
|
||||||
items = append(items, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "new":
|
case "new":
|
||||||
items = []Bookmark{}
|
items = filterBookmarks(reading, func(b Bookmark) bool { return b.HasNewChapter() })
|
||||||
for _, b := range all {
|
case "archived":
|
||||||
if b.HasNewChapter() {
|
items = filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusArchived })
|
||||||
items = append(items, b)
|
case "finished":
|
||||||
}
|
items = filterBookmarks(all, func(b Bookmark) bool { return b.Status == statusFinished })
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
tab = "all"
|
tab = "all"
|
||||||
|
items = reading
|
||||||
}
|
}
|
||||||
return listView{Tab: tab, Recent: recent, Items: items}, nil
|
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")
|
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