aboutsummaryrefslogtreecommitdiff
path: root/internal/web/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/server.go')
-rw-r--r--internal/web/server.go94
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