Render one chapter format, not "Ch Chapter 250"
The userscript writes last_chapter/latest_chapter as "Chapter N" and the poller writes the same label, so the templates' own "Ch " prefix doubled it on every real row. A manual edit through the web UI stores a bare "250", so the list carried two formats at once and stopped being scannable. Move the formatting out of the templates into DisplayChapter/DisplayLatest on Bookmark, which strip a leading "Chapter"/"Ch." lead-in and re-add exactly one "Ch ", falling back to the numeric column when the label is empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
@@ -41,6 +42,41 @@ func (b Bookmark) HasNewChapter() bool {
|
|||||||
return b.LatestChapterNum != nil && *b.LatestChapterNum > b.LastChapterNum
|
return b.LatestChapterNum != nil && *b.LatestChapterNum > b.LastChapterNum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chapterLeadIn matches the prefix the userscript and the poller both write
|
||||||
|
// ("Chapter 250"), so the UI can add exactly one "Ch " of its own instead of
|
||||||
|
// doubling it. A manual edit through the web UI stores a bare "250", which is
|
||||||
|
// the same string minus the lead-in.
|
||||||
|
var chapterLeadIn = regexp.MustCompile(`(?i)^\s*(?:chapter|ch\.?)\s*`)
|
||||||
|
|
||||||
|
func displayChapter(raw string, num float64) string {
|
||||||
|
rest := strings.TrimSpace(chapterLeadIn.ReplaceAllString(raw, ""))
|
||||||
|
if rest == "" {
|
||||||
|
rest = strconv.FormatFloat(num, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
// "Ch " only makes sense in front of a number; anything else is a label the
|
||||||
|
// site gave us, so pass it through as written.
|
||||||
|
if rest[0] < '0' || rest[0] > '9' {
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
return "Ch " + rest
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayChapter is the read-progress line: one canonical "Ch N" whatever
|
||||||
|
// format the write came in as.
|
||||||
|
func (b Bookmark) DisplayChapter() string {
|
||||||
|
return displayChapter(b.LastChapter, b.LastChapterNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayLatest is the same for the newest published chapter, which arrives
|
||||||
|
// with the same "Chapter N" lead-in from both the userscript and the poller.
|
||||||
|
func (b Bookmark) DisplayLatest() string {
|
||||||
|
var num float64
|
||||||
|
if b.LatestChapterNum != nil {
|
||||||
|
num = *b.LatestChapterNum
|
||||||
|
}
|
||||||
|
return displayChapter(b.LatestChapter, num)
|
||||||
|
}
|
||||||
|
|
||||||
// ContinueURL is where the Continue button points: the chapter last read, or
|
// ContinueURL is where the Continue button points: the chapter last read, or
|
||||||
// the series page when no chapter URL was ever captured.
|
// the series page when no chapter URL was ever captured.
|
||||||
func (b Bookmark) ContinueURL() string {
|
func (b Bookmark) ContinueURL() string {
|
||||||
|
|||||||
@@ -1034,3 +1034,41 @@ func TestOpenStoreMigratesAsuraHashlessCollision(t *testing.T) {
|
|||||||
t.Fatalf("merge kept wrong row: %+v", merged)
|
t.Fatalf("merge kept wrong row: %+v", merged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDisplayChapter(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
num float64
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"userscript label", "Chapter 250", 250, "Ch 250"},
|
||||||
|
{"abbreviated with dot", "Ch. 12", 12, "Ch 12"},
|
||||||
|
{"lowercase abbreviation", "ch 3", 3, "Ch 3"},
|
||||||
|
{"bare number from a manual edit", "45.5", 45.5, "Ch 45.5"},
|
||||||
|
{"empty falls back to the number", "", 7, "Ch 7"},
|
||||||
|
{"empty and zero", "", 0, "Ch 0"},
|
||||||
|
{"label with no number passes through", "Prologue", 0, "Prologue"},
|
||||||
|
{"title-like label keeps its words", "Chapter of the Sun", 0, "of the Sun"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := displayChapter(tc.raw, tc.num); got != tc.want {
|
||||||
|
t.Errorf("displayChapter(%q, %v) = %q, want %q", tc.raw, tc.num, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
b := Bookmark{LastChapter: "Chapter 9", LastChapterNum: 9}
|
||||||
|
if got := b.DisplayChapter(); got != "Ch 9" {
|
||||||
|
t.Errorf("DisplayChapter() = %q, want %q", got, "Ch 9")
|
||||||
|
}
|
||||||
|
if got := b.DisplayLatest(); got != "Ch 0" {
|
||||||
|
t.Errorf("DisplayLatest() with nothing captured = %q, want %q", got, "Ch 0")
|
||||||
|
}
|
||||||
|
num := 11.0
|
||||||
|
b.LatestChapter, b.LatestChapterNum = "Chapter 11", &num
|
||||||
|
if got := b.DisplayLatest(); got != "Ch 11" {
|
||||||
|
t.Errorf("DisplayLatest() = %q, want %q", got, "Ch 11")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
{{else if .Favorite}}<span class="foot-rule brass"></span>{{end}}
|
{{else if .Favorite}}<span class="foot-rule brass"></span>{{end}}
|
||||||
</span>
|
</span>
|
||||||
<span class="recent-title">{{.Title}}</span>
|
<span class="recent-title">{{.Title}}</span>
|
||||||
<span class="recent-chapter">Ch {{.LastChapter}}{{if .HasNewChapter}} · New{{end}}</span>
|
<span class="recent-chapter">{{.DisplayChapter}}{{if .HasNewChapter}} · New{{end}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
<p class="meta">
|
<p class="meta">
|
||||||
<span class="site-{{.Site}}">{{.Site}}</span>
|
<span class="site-{{.Site}}">{{.Site}}</span>
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<span class="chapter">Ch {{.LastChapter}}</span>
|
<span class="chapter">{{.DisplayChapter}}</span>
|
||||||
{{if and (eq .Status "reading") .HasNewChapter}}
|
{{if and (eq .Status "reading") .HasNewChapter}}
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<span class="new-chapter">Ch {{.LatestChapter}} out</span>
|
<span class="new-chapter">{{.DisplayLatest}} out</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if eq .Status "archived"}}
|
{{if eq .Status "archived"}}
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
hx-post="/ui/bookmarks/{{.Key}}/chapter"
|
hx-post="/ui/bookmarks/{{.Key}}/chapter"
|
||||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="input, button">
|
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="input, button">
|
||||||
{{if .LatestChapter}}<p class="hint">Latest known: Ch {{.LatestChapter}}</p>{{end}}
|
{{if .LatestChapter}}<p class="hint">Latest known: {{.DisplayLatest}}</p>{{end}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{/* max is a fat-finger guard, not a real ceiling — no series is near it. */}}
|
{{/* max is a fat-finger guard, not a real ceiling — no series is near it. */}}
|
||||||
<input name="chapter" type="number" step="0.1" min="0" max="9999"
|
<input name="chapter" type="number" step="0.1" min="0" max="9999"
|
||||||
|
|||||||
Reference in New Issue
Block a user