-
+
+
{{.Meta.Title}}
{{if .Meta.SourceURL}}
+
+
+
Chapters
-
+
+`
func (s *Server) handleBook(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
@@ -542,17 +612,116 @@ func (s *Server) handleBook(w http.ResponseWriter, r *http.Request) {
return
}
+ total := len(chapters)
+ page := chapters
+ hasMore := false
+ if total > chapterPageSize {
+ page = chapters[:chapterPageSize]
+ hasMore = true
+ }
+
t := template.Must(template.New("book").Parse(bookTmpl))
var buf bytes.Buffer
_ = t.Execute(&buf, struct {
- Slug string
- Meta interface{}
- Chapters interface{}
- }{Slug: slug, Meta: meta, Chapters: chapters})
+ Slug string
+ Meta interface{}
+ Chapters interface{}
+ TotalDownloaded int
+ HasMore bool
+ NextPage int
+ }{
+ Slug: slug,
+ Meta: meta,
+ Chapters: page,
+ TotalDownloaded: total,
+ HasMore: hasMore,
+ NextPage: 2,
+ })
s.respond(w, r, meta.Title, buf.String())
}
+// ─── GET /books/{slug}/chapters-page — paginated chapter list fragment ────────
+
+const chapterPageTmpl = `{{range .Chapters}}
+
+
+ {{.Number}}
+
+ {{.Title}}
+ {{if .Date}}{{.Date}}{{end}}
+
+ ●
+
+
+{{end}}
+{{if .HasMore}}
+
+
+
+
+
+{{else}}
+
+{{end}}`
+
+func (s *Server) handleBookChaptersPage(w http.ResponseWriter, r *http.Request) {
+ slug := r.PathValue("slug")
+ page := 1
+ if p := r.URL.Query().Get("page"); p != "" {
+ if n, err := strconv.Atoi(p); err == nil && n > 0 {
+ page = n
+ }
+ }
+
+ chapters, err := s.writer.ListChapters(slug)
+ if err != nil {
+ http.Error(w, "failed to list chapters: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ start := (page - 1) * chapterPageSize
+ if start >= len(chapters) {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+ end := start + chapterPageSize
+ hasMore := end < len(chapters)
+ if end > len(chapters) {
+ end = len(chapters)
+ }
+
+ t := template.Must(template.New("chapterPage").Parse(chapterPageTmpl))
+ var buf bytes.Buffer
+ _ = t.Execute(&buf, struct {
+ Slug string
+ Chapters interface{}
+ HasMore bool
+ NextPage int
+ }{
+ Slug: slug,
+ Chapters: chapters[start:end],
+ HasMore: hasMore,
+ NextPage: page + 1,
+ })
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = buf.WriteTo(w)
+}
+
// ─── GET /books/{slug}/chapters/{n} — chapter reader ─────────────────────────
const chapterTmpl = `
@@ -683,6 +852,18 @@ const chapterTmpl = `
var KOKORO_URL = '{{.KokoroURL}}';
var NEXT_N = {{.NextN}};
var SLUG = '{{.Slug}}';
+ var CHAPTER_N = {{.ChapterN}};
+
+ // ── reading progress ──────────────────────────────────────────────────────────
+ // Save current chapter to localStorage so the book page can show a resume button.
+ (function saveProgress() {
+ var LS_KEY = 'reading_progress';
+ try {
+ var progress = JSON.parse(localStorage.getItem(LS_KEY) || '{}');
+ progress[SLUG] = CHAPTER_N;
+ localStorage.setItem(LS_KEY, JSON.stringify(progress));
+ } catch(_) {}
+ })();
var audio = document.getElementById('tts-audio');
var btn = document.getElementById('tts-btn');