diff options
| -rw-r--r-- | internal/bible/booktable.go | 4 | ||||
| -rw-r--r-- | internal/config/config.go | 74 | ||||
| -rw-r--r-- | internal/config/config_test.go | 25 | ||||
| -rw-r--r-- | internal/web/server.go | 193 | ||||
| -rw-r--r-- | internal/web/server_test.go | 68 | ||||
| -rw-r--r-- | internal/web/static/base.css | 7 | ||||
| -rw-r--r-- | internal/web/templates/index.html | 1 | ||||
| -rw-r--r-- | internal/web/templates/reader.html | 1 | ||||
| -rw-r--r-- | internal/web/templates/settings.html | 115 |
9 files changed, 481 insertions, 7 deletions
diff --git a/internal/bible/booktable.go b/internal/bible/booktable.go index 007e75e..9ec95be 100644 --- a/internal/bible/booktable.go +++ b/internal/bible/booktable.go @@ -88,6 +88,10 @@ func mustReadEmbedded() []byte { return b } +// DefaultBooksTOML returns the embedded default books.toml (the seed the +// /settings editor shows when the user has no override yet). +func DefaultBooksTOML() []byte { return mustReadEmbedded() } + func parseBooks(data []byte) (map[string]map[string][]string, error) { var raw map[string]map[string][]string if err := toml.Unmarshal(data, &raw); err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 3428ae8..a4d2c3e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "github.com/pelletier/go-toml/v2" @@ -24,7 +25,7 @@ var seedTOML []byte // Version is lectio's release version, shared by every binary's // -v/--version output (lectio, lectio-ui, lectio-web). -const Version = "0.13.1" +const Version = "0.14.0" // validVersions are the five scripture versions lectio understands. var validVersions = map[string]bool{ @@ -300,3 +301,74 @@ func validate(cfg Config) error { } return nil } + +// Validate exposes the semantic checks Load applies, so callers that build a +// Config (the web /settings form) can reject an invalid one before saving. +func Validate(cfg Config) error { return validate(cfg) } + +// Save writes cfg to the config file (configPath, honoring LECTIO_CONFIG), +// creating the directory if needed, as commented TOML (regenerated -- the +// explanatory comments are kept, the values are cfg's). +func Save(cfg Config) error { + path, _, err := configPath() + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, renderConfigTOML(cfg), 0o644) +} + +func tomlStr(s string) string { return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` } + +func tomlStrSlice(ss []string) string { + q := make([]string, len(ss)) + for i, s := range ss { + q[i] = tomlStr(s) + } + return "[" + strings.Join(q, ", ") + "]" +} + +// renderConfigTOML regenerates config.toml from cfg, preserving the seed's +// field comments. Parts (the per-lectionary hide map) is written back verbatim +// so a config that had [parts.*] sections keeps them across a Save. +func renderConfigTOML(cfg Config) []byte { + var b strings.Builder + fmt.Fprintf(&b, "schema_version = %d\n", cfg.SchemaVersion) + fmt.Fprintf(&b, "lectionary = %s # \"new\" (niedziela.pl) or \"traditional\" (missalemeum, 1962)\n", tomlStr(cfg.Lectionary)) + fmt.Fprintf(&b, "traditional_lang = %s # vernacular for traditional propers: \"pl\" or \"en\"\n", tomlStr(cfg.TraditionalLang)) + fmt.Fprintf(&b, "versions = %s # compare set + TUI cycle order (bt = Biblia Tysiąclecia)\n", tomlStrSlice(cfg.Versions)) + fmt.Fprintf(&b, "default_version = %s # TUI start / `lectio -b` default\n", tomlStr(cfg.DefaultVersion)) + fmt.Fprintf(&b, "width = %d # CLI wrap width; 0 = detect terminal\n", cfg.Width) + fmt.Fprintf(&b, "all = %t # all parts (true) or just the gospel (false)\n", cfg.All) + fmt.Fprintf(&b, "offline = %t # true = never fetch; read only cache + harvested sigla\n", cfg.Offline) + fmt.Fprintf(&b, "ui_language = %s # interface language: \"en\" or \"pl\" (readings stay source-language)\n", tomlStr(cfg.UILanguage)) + fmt.Fprintf(&b, "sigla_style = %s # citation dialect for --ref/--list/--citation: \"auto\", \"polish\", \"english\"\n", tomlStr(cfg.SiglaStyle)) + fmt.Fprintf(&b, "web_theme = %s # default lectio-web theme\n", tomlStr(cfg.WebTheme)) + fmt.Fprintf(&b, "web_port = %d # lectio-web port; 0 = try 1099, then any free port\n", cfg.WebPort) + fmt.Fprintf(&b, "web_display = %s # lectio-web layout: \"horizontal\", \"vertical\", \"interlinear\"\n", tomlStr(cfg.WebDisplay)) + fmt.Fprintf(&b, "web_mono = %t # lectio-web: monospace reading face\n", cfg.WebMono) + fmt.Fprintf(&b, "web_versions = %s # which versions start toggled on; empty = just default_version\n", tomlStrSlice(cfg.WebVersions)) + fmt.Fprintf(&b, "pager = %s # pager for `lectio` output; \"\" = off\n", tomlStr(cfg.Pager)) + + // Preserve the parts map (not edited by the form) so [parts.*] survives Save. + lects := make([]string, 0, len(cfg.Parts)) + for l := range cfg.Parts { + lects = append(lects, l) + } + sort.Strings(lects) + for _, l := range lects { + parts := cfg.Parts[l] + keys := make([]string, 0, len(parts)) + for k := range parts { + keys = append(keys, k) + } + sort.Strings(keys) + fmt.Fprintf(&b, "\n[parts.%s]\n", l) + for _, k := range keys { + fmt.Fprintf(&b, "%s = %t\n", k, parts[k]) + } + } + return []byte(b.String()) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index cc0c941..53a3980 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -199,6 +199,31 @@ func TestSiglaStyleDefaultsAndDialect(t *testing.T) { } } +func TestSaveRoundTrip(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.toml") + t.Setenv("LECTIO_CONFIG", path) + + cfg := Default() + cfg.UILanguage = "pl" + cfg.SiglaStyle = "english" + cfg.WebMono = true + cfg.WebVersions = []string{"wuj", "vul"} + cfg.Pager = "less -R" + cfg.Parts = map[string]map[string]bool{"new": {"psalm": false}} + if err := Save(cfg); err != nil { + t.Fatal(err) + } + got, err := Load() + if err != nil { + t.Fatalf("reload: %v", err) + } + if got.UILanguage != "pl" || got.SiglaStyle != "english" || !got.WebMono || + len(got.WebVersions) != 2 || got.Pager != "less -R" || got.PartShown("new", "psalm") { + t.Errorf("round-trip mismatch: %+v", got) + } +} + func TestPartShown(t *testing.T) { var empty Config if !empty.PartShown("new", "psalm") { 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 diff --git a/internal/web/server_test.go b/internal/web/server_test.go index 5f15796..ee36318 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -4,11 +4,13 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "strings" "testing" + "github.com/lukaszkasprzak/lectio/internal/bible" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/liturgy" ) @@ -499,3 +501,69 @@ func TestReaderArrowsDriveChapSelect(t *testing.T) { t.Errorf("server did not render the requested Luke 12") } } + +func TestSettingsGet(t *testing.T) { + srv := NewServer(config.Default()) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, httptest.NewRequest("GET", "/settings", nil)) + if rec.Code != 200 { + t.Fatalf("status %d", rec.Code) + } + b := rec.Body.String() + if !strings.Contains(b, `name="ui_language"`) || !strings.Contains(b, `name="books"`) { + t.Errorf("settings form missing fields") + } +} + +func TestSettingsPostAppliesLive(t *testing.T) { + t.Setenv("LECTIO_CONFIG", filepath.Join(t.TempDir(), "config.toml")) + srv := NewServer(config.Default()) // starts ui_language=en + + form := url.Values{} + form.Set("lectionary", "new") + form.Set("traditional_lang", "pl") + form.Set("ui_language", "pl") // change it + form.Set("sigla_style", "auto") + form.Set("web_display", "vertical") + form.Set("web_theme", "transfiguration") + form.Set("default_version", "bt") + form["versions"] = []string{"bt", "wuj", "vul", "grb", "drb"} + form.Set("books", string(bibleDefaultBooks())) + + post := httptest.NewRequest("POST", "/settings", strings.NewReader(form.Encode())) + post.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, post) + if rec.Code != http.StatusSeeOther { + t.Fatalf("post status %d, want 303", rec.Code) + } + + // Live-applied: the daily page now renders Polish chrome (ui_language=pl). + idx := httptest.NewRecorder() + srv.ServeHTTP(idx, httptest.NewRequest("GET", "/", nil)) + if !strings.Contains(idx.Body.String(), `lang="pl"`) { + t.Errorf("ui_language change not applied live") + } +} + +func TestSettingsPostInvalidBooks(t *testing.T) { + t.Setenv("LECTIO_CONFIG", filepath.Join(t.TempDir(), "config.toml")) + srv := NewServer(config.Default()) + form := url.Values{} + form.Set("lectionary", "new") + form.Set("ui_language", "en") + form.Set("web_display", "vertical") + form.Set("web_theme", "transfiguration") + form.Set("default_version", "bt") + form["versions"] = []string{"bt"} + form.Set("books", "this is not [valid toml") + post := httptest.NewRequest("POST", "/settings", strings.NewReader(form.Encode())) + post.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, post) + if rec.Code != 200 || !strings.Contains(rec.Body.String(), "books.toml") { + t.Errorf("invalid books should re-render with an error, got %d", rec.Code) + } +} + +func bibleDefaultBooks() []byte { return bible.DefaultBooksTOML() } diff --git a/internal/web/static/base.css b/internal/web/static/base.css index a233e9d..c8e1bd4 100644 --- a/internal/web/static/base.css +++ b/internal/web/static/base.css @@ -278,3 +278,10 @@ a { grid-template-columns: 1fr; } } + +/* Settings form — a simple labeled column; themes own colour. */ +.settings { display: flex; flex-direction: column; gap: var(--space-2); max-width: 40rem; font-family: var(--font-ui); } +.settings label { display: flex; flex-direction: column; gap: 0.25rem; align-items: flex-start; } +.settings .row { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; } +.settings textarea { width: 100%; font-family: var(--font-mono); } +.settings .saved { font-family: var(--font-ui); } diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html index 6920fcb..3058746 100644 --- a/internal/web/templates/index.html +++ b/internal/web/templates/index.html @@ -62,6 +62,7 @@ </label> <a class="nav-link" href="/reader">{{.L.NavReader}} →</a> + <a class="nav-link" href="/settings">settings</a> </form> <label class="theme-picker">{{.L.Theme}} diff --git a/internal/web/templates/reader.html b/internal/web/templates/reader.html index 921016a..af29e65 100644 --- a/internal/web/templates/reader.html +++ b/internal/web/templates/reader.html @@ -20,6 +20,7 @@ <form class="controls" action="/reader" method="get" hx-get="/reader" hx-target="#reader-root" hx-select="#reader-root" hx-swap="outerHTML" hx-trigger="change"> <a class="nav-link" href="/">← {{.L.BannerReadings}}</a> + <a class="nav-link" href="/settings">settings</a> <label>{{.L.WebBook}} <select name="book"> diff --git a/internal/web/templates/settings.html b/internal/web/templates/settings.html new file mode 100644 index 0000000..bea6fb9 --- /dev/null +++ b/internal/web/templates/settings.html @@ -0,0 +1,115 @@ +{{/* settings.html — lectio-web /settings page: a form for every config + setting plus a raw books.toml editor. A plain POST form (no htmx) -- + saving redirects (PRG) back here with ?saved=1. On a validation error + the form re-renders with the submitted values and an inline message + instead of redirecting. Self-contained and theme-aware, like index.html. */}} +<!doctype html> +<html lang="{{.Lang}}"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>lectio — settings</title> +<link rel="stylesheet" href="/static/base.css"> +<link id="theme" rel="stylesheet" href="/theme.css?name={{.Cfg.WebTheme}}"> +<script src="/static/htmx.min.js"></script> +</head> +<body{{if .Cfg.WebMono}} class="mono"{{end}}> +<div class="page"> + + <p><a class="nav-link" href="/">← {{.L.BannerReadings}}</a></p> + + {{if .Saved}}<p class="saved">Saved.</p>{{end}} + {{if .Error}}<p class="error">{{.Error}}</p>{{end}} + + <form class="settings" method="post" action="/settings"> + + <label>{{.L.Lectionary}} + <select name="lectionary"> + <option value="new" {{if eq .Cfg.Lectionary "new"}}selected{{end}}>{{.L.OptModern}}</option> + <option value="traditional" {{if eq .Cfg.Lectionary "traditional"}}selected{{end}}>{{.L.OptTraditional}}</option> + </select> + </label> + + <label>traditional language + <select name="traditional_lang"> + <option value="pl" {{if eq .Cfg.TraditionalLang "pl"}}selected{{end}}>pl</option> + <option value="en" {{if eq .Cfg.TraditionalLang "en"}}selected{{end}}>en</option> + </select> + </label> + + <label>UI language + <select name="ui_language"> + <option value="en" {{if eq .Cfg.UILanguage "en"}}selected{{end}}>en</option> + <option value="pl" {{if eq .Cfg.UILanguage "pl"}}selected{{end}}>pl</option> + </select> + </label> + + <label>sigla style + <select name="sigla_style"> + <option value="auto" {{if eq .Cfg.SiglaStyle "auto"}}selected{{end}}>auto</option> + <option value="polish" {{if eq .Cfg.SiglaStyle "polish"}}selected{{end}}>polish</option> + <option value="english" {{if eq .Cfg.SiglaStyle "english"}}selected{{end}}>english</option> + </select> + </label> + + <label>{{.L.Layout}} + <select name="web_display"> + <option value="horizontal" {{if eq .Cfg.WebDisplay "horizontal"}}selected{{end}}>{{.L.OptHorizontal}}</option> + <option value="vertical" {{if eq .Cfg.WebDisplay "vertical"}}selected{{end}}>{{.L.OptColumns}}</option> + <option value="interlinear" {{if eq .Cfg.WebDisplay "interlinear"}}selected{{end}}>{{.L.OptInterlinear}}</option> + </select> + </label> + + <label>{{.L.Theme}} + <select name="web_theme"> + {{range .ThemeOpts}}<option value="{{.Name}}" {{if .Selected}}selected{{end}}>{{.Name}}</option>{{end}} + </select> + </label> + + <label>default version + <select name="default_version"> + {{range .VersionOpts}}<option value="{{.Code}}" {{if eq .Code $.Cfg.DefaultVersion}}selected{{end}}>{{.Code}}</option>{{end}} + </select> + </label> + + <div class="row"> + versions + {{range .VersionOpts}}<label><input type="checkbox" name="versions" value="{{.Code}}" {{if .Checked}}checked{{end}}> {{.Code}}</label>{{end}} + </div> + + <div class="row"> + web versions + {{range .WebVersionOpts}}<label><input type="checkbox" name="web_versions" value="{{.Code}}" {{if .Checked}}checked{{end}}> {{.Code}}</label>{{end}} + </div> + + <div class="row"> + <label><input type="checkbox" name="all" {{if .Cfg.All}}checked{{end}}> {{.L.OptAll}}</label> + <label><input type="checkbox" name="offline" {{if .Cfg.Offline}}checked{{end}}> offline</label> + <label><input type="checkbox" name="web_mono" {{if .Cfg.WebMono}}checked{{end}}> {{.L.Mono}}</label> + </div> + + <label>width + <input type="number" name="width" value="{{.Cfg.Width}}"> + </label> + + <label>web port + <input type="number" name="web_port" value="{{.Cfg.WebPort}}"> + </label> + + <label>pager + <input type="text" name="pager" value="{{.Cfg.Pager}}"> + </label> + + <label>books.toml + <textarea name="books" rows="20">{{.Books}}</textarea> + </label> + + <div class="row"> + <button type="submit">Save</button> + </div> + + </form> + +</div> +</body> +</html> |
