diff --git a/scraper/internal/server/server.go b/scraper/internal/server/server.go index 68105ae..7f45390 100644 --- a/scraper/internal/server/server.go +++ b/scraper/internal/server/server.go @@ -62,6 +62,7 @@ func (s *Server) ListenAndServe(ctx context.Context) error { mux.HandleFunc("GET /ranking/view", s.handleRankingView) mux.HandleFunc("GET /books/{slug}", s.handleBook) mux.HandleFunc("GET /books/{slug}/chapters/{n}", s.handleChapter) + mux.HandleFunc("GET /books/{slug}/chapters-page", s.handleBookChaptersPage) mux.HandleFunc("POST /ui/scrape/book", s.handleUIScrapeBook) mux.HandleFunc("GET /ui/scrape/status", s.handleUIScrapeStatus) // Plain-text chapter content for browser-side TTS diff --git a/scraper/internal/server/ui.go b/scraper/internal/server/ui.go index ad217c1..f49a31b 100644 --- a/scraper/internal/server/ui.go +++ b/scraper/internal/server/ui.go @@ -459,6 +459,8 @@ func (s *Server) handleRankingView(w http.ResponseWriter, r *http.Request) { // ─── GET /books/{slug} — chapter list ──────────────────────────────────────── +const chapterPageSize = 50 + const bookTmpl = `
{{end}} -
- ` + + {{if .HasMore}} +
+ +
+ {{end}} +
+ +` 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');