diff --git a/scraper/internal/server/ui.go b/scraper/internal/server/ui.go index cd64dc7..25a66c9 100644 --- a/scraper/internal/server/ui.go +++ b/scraper/internal/server/ui.go @@ -13,6 +13,7 @@ import ( "time" "github.com/libnovel/scraper/internal/orchestrator" + "github.com/libnovel/scraper/internal/scraper" "github.com/libnovel/scraper/internal/writer" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" @@ -167,6 +168,7 @@ const homeTmpl = `
{{if .Status}}{{.Status}}{{end}} {{if .TotalChapters}}{{.TotalChapters}} ch{{end}} + {{if .Downloaded}}{{.Downloaded}} downloaded{{end}}
@@ -231,6 +233,42 @@ const homeTmpl = ` } } htmx.process(a); + + // Add dismiss button (swipe-left to remove from continue reading). + var dismissBtn = document.createElement('button'); + dismissBtn.type = 'button'; + dismissBtn.title = 'Remove from continue reading'; + dismissBtn.className = 'dismiss-btn absolute top-2 right-2 z-10 flex items-center justify-center w-6 h-6 rounded-full bg-zinc-700 hover:bg-zinc-600 text-zinc-400 hover:text-zinc-100 transition-colors opacity-0 group-hover:opacity-100'; + dismissBtn.innerHTML = ''; + dismissBtn.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + var cardEl = this.closest('[data-slug]'); + var cardSlug = cardEl ? cardEl.dataset.slug : null; + if (cardSlug) { + var p = {}; + try { p = JSON.parse(localStorage.getItem('reading_progress') || '{}'); } catch(_) {} + delete p[cardSlug]; + localStorage.setItem('reading_progress', JSON.stringify(p)); + } + // Slide left and fade out, then remove. + var target = cardEl || this.parentElement; + target.style.transition = 'transform 0.25s ease, opacity 0.25s ease'; + target.style.transform = 'translateX(-60px)'; + target.style.opacity = '0'; + setTimeout(function() { + target.remove(); + // Hide section if no cards remain. + var remaining = continueGrid.querySelectorAll('[data-slug]'); + if (remaining.length === 0) { + continueSection.classList.add('hidden'); + } + }, 260); + }); + // Make card position:relative so the absolute button positions correctly. + a.style.position = 'relative'; + a.appendChild(dismissBtn); + continueGrid.appendChild(a); // Keep the original card visible in #books-grid — do not hide it. // Books should always appear in Available regardless of reading progress. @@ -268,6 +306,12 @@ const homeTmpl = ` }()); ` +// homeBookItem wraps BookMeta with the count of chapters already on disk. +type homeBookItem struct { + scraper.BookMeta + Downloaded int +} + func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) @@ -280,12 +324,20 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { return } + items := make([]homeBookItem, len(books)) + for i, b := range books { + items[i] = homeBookItem{ + BookMeta: b, + Downloaded: s.writer.CountChapters(b.Slug), + } + } + t := template.Must(template.New("home").Parse(homeTmpl)) var buf bytes.Buffer _ = t.Execute(&buf, struct { Books interface{} }{ - Books: books, + Books: items, }) s.respond(w, r, "Home", buf.String()) @@ -1262,9 +1314,19 @@ const bookTmpl = ` ← All books + + +
{{if .Meta.Cover}} - cover + cover {{end}}
@@ -1311,6 +1373,20 @@ const bookTmpl = `
+ + {{if .LastChapter}} + + {{end}} +

Chapters