diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-24 15:16:47 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-24 15:16:47 +0200 |
| commit | 1e4939874a14d8abb13e82b2273bfc5f9d653295 (patch) | |
| tree | 9c9eecb37fad3dc78d5bd8f58bc405d6f9cbd672 /internal/web/server.go | |
| parent | e6665bd3f9ae011f65788004e4eab4472a461e8f (diff) | |
| download | lectio-1e4939874a14d8abb13e82b2273bfc5f9d653295.tar.gz lectio-1e4939874a14d8abb13e82b2273bfc5f9d653295.zip | |
bookmarks: web reader bookmarks with notes + tags (add/list/filter/jump/delete); v0.15.0
Diffstat (limited to 'internal/web/server.go')
| -rw-r--r-- | internal/web/server.go | 94 |
1 files changed, 93 insertions, 1 deletions
diff --git a/internal/web/server.go b/internal/web/server.go index d7f0ebc..cd50e5a 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -12,6 +12,7 @@ import ( "io/fs" "net" "net/http" + "net/url" "os" "os/exec" "regexp" @@ -22,6 +23,7 @@ import ( "time" "github.com/lukaszkasprzak/lectio/internal/bible" + "github.com/lukaszkasprzak/lectio/internal/bookmarks" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/i18n" "github.com/lukaszkasprzak/lectio/internal/liturgy" @@ -40,6 +42,7 @@ type server struct { mu sync.RWMutex cfg config.Config tbl *bible.BookTable + bm *bookmarks.Store } func (s *server) get() config.Config { s.mu.RLock(); defer s.mu.RUnlock(); return s.cfg } @@ -59,7 +62,7 @@ func (s *server) apply(cfg config.Config, tbl *bible.BookTable) { // live, mutable copy held by server, which every handler reads per request. func NewServer(cfg config.Config) http.Handler { tbl, _ := bible.LoadBookTable(config.UserBooksTOML()) - s := &server{cfg: cfg, tbl: tbl} + s := &server{cfg: cfg, tbl: tbl, bm: bookmarks.Open()} mux := http.NewServeMux() mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { indexHandler(s.get())(w, r) }) @@ -68,6 +71,9 @@ func NewServer(cfg config.Config) http.Handler { mux.HandleFunc("GET /theme.css", func(w http.ResponseWriter, r *http.Request) { themeCSSHandler(s.get())(w, r) }) mux.HandleFunc("GET /settings", settingsGet(s)) mux.HandleFunc("POST /settings", settingsPost(s)) + mux.HandleFunc("POST /reader/bookmark", addBookmark(s)) + mux.HandleFunc("GET /bookmarks", listBookmarks(s)) + mux.HandleFunc("POST /bookmarks/delete", deleteBookmark(s)) staticSub, err := fs.Sub(staticFS, "static") if err != nil { @@ -403,6 +409,7 @@ func atoiDefault(s string, def int) int { type readerData struct { BookOpts []bookOpt + Book string // canonical name of the selected book (for the bookmark-add form) Chap int PrevChap, NextChap int ChapOpts []chapOpt @@ -488,6 +495,7 @@ func readerHandler(cfg config.Config, tbl *bible.BookTable) http.HandlerFunc { data := readerData{ BookOpts: bookOpts, + Book: info.Canonical, Chap: chap, PrevChap: prev, NextChap: next, @@ -509,6 +517,90 @@ func readerHandler(cfg config.Config, tbl *bible.BookTable) http.HandlerFunc { } } +// addBookmark saves the current reader book+chapter with a note and tags, then +// returns to the reader at that location. +func addBookmark(s *server) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + book := r.PostForm.Get("book") + chap := atoiOr(r.PostForm.Get("chap"), 0) + if book != "" && chap > 0 { + _, _ = s.bm.Add(bookmarks.Bookmark{ + Book: book, + Chapter: chap, + Note: strings.TrimSpace(r.PostForm.Get("note")), + Tags: bookmarks.ParseTags(r.PostForm.Get("tags")), + }) + } + http.Redirect(w, r, "/reader?book="+url.QueryEscape(book)+"&chap="+strconv.Itoa(chap), http.StatusSeeOther) + } +} + +// bookmarkView is one row of templates/bookmarks.html. +type bookmarkView struct { + ID string + Book string // canonical (for the ?book= open link) + Display string // dialect display name + Chapter int + Note string + Tags []string + Created string +} + +type bookmarksData struct { + Items []bookmarkView + AllTags []string + Tag string // active filter ("" = all) + L i18n.UI + Lang string +} + +// listBookmarks renders the bookmarks page, optionally filtered by ?tag=. +func listBookmarks(s *server) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cfg := s.get() + tag := r.URL.Query().Get("tag") + items, err := s.bm.List(tag) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + names := map[string]string{} + for _, b := range s.table().Books(cfg.SiglaLang()) { + names[b.Canonical] = b.Name + } + views := make([]bookmarkView, 0, len(items)) + for _, b := range items { + disp := names[b.Book] + if disp == "" { + disp = b.Book + } + views = append(views, bookmarkView{ID: b.ID, Book: b.Book, Display: disp, Chapter: b.Chapter, Note: b.Note, Tags: b.Tags, Created: b.Created}) + } + allTags, _ := s.bm.Tags() + data := bookmarksData{Items: views, AllTags: allTags, Tag: tag, L: i18n.Get(cfg.UILanguage), Lang: cfg.UILanguage} + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.ExecuteTemplate(w, "bookmarks.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +// deleteBookmark removes a bookmark and returns to the list. +func deleteBookmark(s *server) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + _ = s.bm.Delete(r.PostForm.Get("id")) + http.Redirect(w, r, "/bookmarks", http.StatusSeeOther) + } +} + // themeCSSHandler serves one theme's stylesheet: the requested name, or // (unknown/invalid) cfg.WebTheme, or (that also unknown) the built-in // default -- so an unrecognized ?name= degrades to a working theme instead |
