diff options
Diffstat (limited to 'internal/web/server.go')
| -rw-r--r-- | internal/web/server.go | 193 |
1 files changed, 187 insertions, 6 deletions
diff --git a/internal/web/server.go b/internal/web/server.go index 7c90e9e..34371b7 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -12,11 +12,13 @@ import ( "io/fs" "net" "net/http" + "os" "os/exec" "regexp" "runtime" "strconv" "strings" + "sync" "time" "github.com/lukaszkasprzak/lectio/internal/bible" @@ -32,18 +34,40 @@ import ( // every visitor sees the same five choices regardless of their config file. var bibleVersions = []string{"bt", "wuj", "vul", "grb", "drb"} +// server holds the live, mutable config + book table so /settings can apply +// changes to the running process. All handlers read a snapshot via get()/table(). +type server struct { + mu sync.RWMutex + cfg config.Config + tbl *bible.BookTable +} + +func (s *server) get() config.Config { s.mu.RLock(); defer s.mu.RUnlock(); return s.cfg } +func (s *server) table() *bible.BookTable { s.mu.RLock(); defer s.mu.RUnlock(); return s.tbl } +func (s *server) apply(cfg config.Config, tbl *bible.BookTable) { + s.mu.Lock() + defer s.mu.Unlock() + s.cfg = cfg + if tbl != nil { + s.tbl = tbl + } +} + // NewServer builds lectio-web's route tree. cfg supplies the defaults // (lectionary, versions, theme, offline) that requests can override via -// query parameters; it is never mutated. +// query parameters; it is never mutated -- /settings applies changes to a +// 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} mux := http.NewServeMux() - - mux.HandleFunc("GET /{$}", indexHandler(cfg)) - mux.HandleFunc("GET /readings", readingsHandler(cfg)) - mux.HandleFunc("GET /reader", readerHandler(cfg, tbl)) - mux.HandleFunc("GET /theme.css", themeCSSHandler(cfg)) + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { indexHandler(s.get())(w, r) }) + mux.HandleFunc("GET /readings", func(w http.ResponseWriter, r *http.Request) { readingsHandler(s.get())(w, r) }) + mux.HandleFunc("GET /reader", func(w http.ResponseWriter, r *http.Request) { readerHandler(s.get(), s.table())(w, r) }) + 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)) staticSub, err := fs.Sub(staticFS, "static") if err != nil { @@ -508,6 +532,163 @@ func themeCSSHandler(cfg config.Config) http.HandlerFunc { } } +// settingsData drives templates/settings.html. +type settingsData struct { + Cfg config.Config + Books string // current books.toml text (user override, else the embedded default) + ThemeOpts []themeOpt // for the web_theme <select> + VersionOpts []versionOpt // all five, Checked if in Cfg.Versions + WebVersionOpts []versionOpt // all five, Checked if in Cfg.WebVersions + Saved bool // show a "saved" note (post-redirect) + Error string // validation error to show (empty = none) + L i18n.UI + Lang string +} + +func newSettingsData(cfg config.Config, books, errMsg string, saved bool) settingsData { + themes := Themes() + opts := make([]themeOpt, 0, len(themes)) + for _, name := range themes { + opts = append(opts, themeOpt{Name: name, Selected: name == cfg.WebTheme}) + } + + versionsSet := map[string]bool{} + for _, v := range cfg.Versions { + versionsSet[v] = true + } + versionOpts := make([]versionOpt, 0, len(bibleVersions)) + for _, v := range bibleVersions { + versionOpts = append(versionOpts, versionOpt{Code: v, Checked: versionsSet[v]}) + } + + webVersionsSet := map[string]bool{} + for _, v := range cfg.WebVersions { + webVersionsSet[v] = true + } + webVersionOpts := make([]versionOpt, 0, len(bibleVersions)) + for _, v := range bibleVersions { + webVersionOpts = append(webVersionOpts, versionOpt{Code: v, Checked: webVersionsSet[v]}) + } + + return settingsData{ + Cfg: cfg, + Books: books, + ThemeOpts: opts, + VersionOpts: versionOpts, + WebVersionOpts: webVersionOpts, + Saved: saved, + Error: errMsg, + L: i18n.Get(cfg.UILanguage), + Lang: cfg.UILanguage, + } +} + +func currentBooksText() string { + if b := config.UserBooksTOML(); len(b) > 0 { + return string(b) + } + return string(bible.DefaultBooksTOML()) +} + +// settingsGet renders the settings form for the live config. +func settingsGet(s *server) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + data := newSettingsData(s.get(), currentBooksText(), "", r.URL.Query().Get("saved") == "1") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.ExecuteTemplate(w, "settings.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +// settingsPost validates and persists the submitted settings + books.toml, then +// applies them to the running server and redirects (PRG). On a validation error +// it re-renders the form with the message and the user's edits, HTTP 200. +func settingsPost(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 + } + + cfg := s.get() // start from live cfg so Parts/SchemaVersion are preserved + cfg.Lectionary = normLect(r.PostForm.Get("lectionary"), cfg.Lectionary) + cfg.TraditionalLang = pickLang(r.PostForm.Get("traditional_lang"), cfg.TraditionalLang) + cfg.UILanguage = config.NormalizeUILanguage(r.PostForm.Get("ui_language")) + cfg.SiglaStyle = config.NormalizeSiglaStyle(r.PostForm.Get("sigla_style")) + cfg.WebDisplay = config.NormalizeDisplay(r.PostForm.Get("web_display")) + cfg.WebTheme = r.PostForm.Get("web_theme") + cfg.DefaultVersion = r.PostForm.Get("default_version") + cfg.Versions = r.PostForm["versions"] + cfg.WebVersions = r.PostForm["web_versions"] + cfg.All = r.PostForm.Get("all") != "" + cfg.Offline = r.PostForm.Get("offline") != "" + cfg.WebMono = r.PostForm.Get("web_mono") != "" + cfg.Width = atoiOr(r.PostForm.Get("width"), cfg.Width) + cfg.WebPort = atoiOr(r.PostForm.Get("web_port"), cfg.WebPort) + cfg.Pager = r.PostForm.Get("pager") + + booksText := r.PostForm.Get("books") + + // Validate config. + if err := config.Validate(cfg); err != nil { + renderSettingsError(w, cfg, booksText, err.Error()) + return + } + // Validate books.toml (parse via the same loader the app uses). + var newTbl *bible.BookTable + if strings.TrimSpace(booksText) != "" { + t, berr := bible.LoadBookTable([]byte(booksText)) + if berr != nil { + renderSettingsError(w, cfg, booksText, "books.toml: "+berr.Error()) + return + } + newTbl = t + } + + // Persist. + if err := config.Save(cfg); err != nil { + renderSettingsError(w, cfg, booksText, "saving config: "+err.Error()) + return + } + if p, err := config.BooksPath(); err == nil { + _ = os.WriteFile(p, []byte(booksText), 0o644) + } + + s.apply(cfg, newTbl) + http.Redirect(w, r, "/settings?saved=1", http.StatusSeeOther) + } +} + +func renderSettingsError(w http.ResponseWriter, cfg config.Config, books, msg string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + _ = tmpl.ExecuteTemplate(w, "settings.html", newSettingsData(cfg, books, msg, false)) +} + +// normLect normalizes a lectionary form value, falling back to def on anything invalid. +func normLect(v, def string) string { + if n, ok := config.NormalizeLectionary(v); ok { + return n + } + return def +} + +// pickLang returns v if it is "pl"/"en", else def. +func pickLang(v, def string) string { + if v == "pl" || v == "en" { + return v + } + return def +} + +func atoiOr(s string, def int) int { + if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil { + return n + } + return def +} + // defaultWebPort is the port chooseListener prefers when cfg.WebPort is 0. const defaultWebPort = 1099 |
