Move to embedded dirs

This commit is contained in:
Andrew-71 2024-05-18 16:15:20 +03:00
parent 119ae9c3c6
commit 430a7461e6
16 changed files with 99 additions and 42 deletions

View file

@ -1,6 +1,20 @@
# Changelog # Changelog
This file keeps track of changes in more human-readable fashion This file keeps track of changes in more human-readable fashion
## v1.0.0
This release includes several **breaking** changes
* Made a new favicon
* English is now used as a fallback language, making incomplete translations somewhat usable
* Added a link to the bottom of the page in day list, for when you need to get to footer but been using the app for months...
* `pages`, `public` and `i18n` directories now use embed.FS
* Running plain executable is now a somewhat valid option.
* A problem with this is that languages and themes are now harder to add. I will think about what to do about that, maybe some kind of `custom.css` file.
I might also be open to GitHub pull requests adding **some** languages (German and French could be nice starting points, I have friends studying them)
* Added a new "sans" theme
* Light blue accent colour
* Comic Sans MS for *everything*
* sorry
## v0.6.0 ## v0.6.0
* Replaced config reload with edit in info (api method still available, config reloads on save) * Replaced config reload with edit in info (api method still available, config reloads on save)
* Bug fixes * Bug fixes

View file

@ -15,10 +15,7 @@ WORKDIR /
# Bring over the executable # Bring over the executable
COPY --from=build-stage /hibiscus /hibiscus COPY --from=build-stage /hibiscus /hibiscus
# Copy files # Data dirs
COPY public public/
COPY pages pages/
COPY i18n i18n/
VOLUME /data VOLUME /data
VOLUME /config VOLUME /config

View file

@ -50,8 +50,8 @@ password=admin # Your password
port=7101 # What port to run on (probably leave on 7101 if using docker) port=7101 # What port to run on (probably leave on 7101 if using docker)
timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse. timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse.
grace_period=0s # Time after a new day begins, but before the switch to next day's file. e.g. 2h30m - files will change at 2:30am grace_period=0s # Time after a new day begins, but before the switch to next day's file. e.g. 2h30m - files will change at 2:30am
language=en # ISO-639 language code (pre-installed - en, ru) language=en # ISO-639 language code (available - en, ru)
theme=default # Picked theme (pre-installed - default, gruvbox, high-contrast) theme=default # Picked theme (available - default, high-contrast, lavender, gruvbox, sans)
title=🌺 Hibiscus.txt # The text in the header title=🌺 Hibiscus.txt # The text in the header
log_to_file=false # Whether to write logs to a file log_to_file=false # Whether to write logs to a file
log_file=config/log.txt # Where to store the log file if it is enabled log_file=config/log.txt # Where to store the log file if it is enabled

View file

@ -1,6 +1,8 @@
# TODO # TODO
List of things to add to this project List of things to add to this project
* Forward/backward buttons for days
## v1.0.0 ## v1.0.0
* a logo so I can enable PWA (and look cool) * a logo so I can enable PWA (and look cool)
* Versioned containers via `ghcr.io` or `dockerhub`, * Versioned containers via `ghcr.io` or `dockerhub`,

View file

@ -144,7 +144,7 @@ func (c *Config) Reload() error {
} }
slog.Debug("reloaded config", "config", c) slog.Debug("reloaded config", "config", c)
return LoadLanguage(c.Language) // Load selected language return SetLanguage(c.Language) // Load selected language
} }
// Read gets raw contents from ConfigFile // Read gets raw contents from ConfigFile

33
i18n.go
View file

@ -1,32 +1,33 @@
package main package main
import ( import (
"embed"
"encoding/json" "encoding/json"
"errors"
"log/slog" "log/slog"
"os"
) )
//go:embed i18n
var I18n embed.FS
var Translations = map[string]string{} var Translations = map[string]string{}
// LoadLanguage loads a json file for selected language into the Translations map // SetLanguage loads a json file for selected language into the Translations map, with english language as a fallback
func LoadLanguage(language string) error { func SetLanguage(language string) error {
filename := "i18n/" + language + ".json" loadLanguage := func(language string) error {
filename := "i18n/" + language + ".json"
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { fileContents, err := I18n.ReadFile(filename)
return err if err != nil {
slog.Error("error reading language file",
"error", err,
"file", filename)
return err
}
return json.Unmarshal(fileContents, &Translations)
} }
err := loadLanguage("en") // Load english as fallback language
fileContents, err := os.ReadFile(filename)
if err != nil { if err != nil {
slog.Error("error reading file",
"error", err,
"file", filename)
return err return err
} }
return loadLanguage(language)
err = json.Unmarshal(fileContents, &Translations)
return err
} }
// TranslatableText attempts to match an id to a string in current language // TranslatableText attempts to match an id to a string in current language

View file

@ -16,6 +16,7 @@
"button.notes": "New note", "button.notes": "New note",
"prompt.notes": "Note name", "prompt.notes": "Note name",
"noscript.notes": "/notes/<name>", "noscript.notes": "/notes/<name>",
"prompt.days": "To the bottom",
"info.version": "Version", "info.version": "Version",
"info.version.link": "source and changelog", "info.version.link": "source and changelog",

View file

@ -16,6 +16,7 @@
"button.notes": "Новая заметка", "button.notes": "Новая заметка",
"prompt.notes": "Название заметки", "prompt.notes": "Название заметки",
"noscript.notes": "/notes/<название>", "noscript.notes": "/notes/<название>",
"prompt.days": "Вниз",
"info.version": "Версия", "info.version": "Версия",
"info.version.link": "исходный код", "info.version.link": "исходный код",

View file

@ -6,7 +6,7 @@ import (
"net/http" "net/http"
) )
var infoTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/info.html")) var infoTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/info.html"))
type AppInfo struct { type AppInfo struct {
Version string Version string
@ -15,7 +15,7 @@ type AppInfo struct {
// Info contains app information // Info contains app information
var Info = AppInfo{ var Info = AppInfo{
Version: "0.6.1", Version: "1.0.0",
SourceLink: "https://git.a71.su/Andrew71/hibiscus", SourceLink: "https://git.a71.su/Andrew71/hibiscus",
} }

View file

@ -34,7 +34,7 @@
{{end}} {{end}}
{{define "footer"}} {{define "footer"}}
<footer> <footer id="footer">
<p><a href="/">{{ translatableText "link.today" }}</a> | <a href="/day">{{ translatableText "link.days" }}</a> | <a href="/notes">{{ translatableText "link.notes" }}</a> <p><a href="/">{{ translatableText "link.today" }}</a> | <a href="/day">{{ translatableText "link.days" }}</a> | <a href="/notes">{{ translatableText "link.notes" }}</a>
<span style="float:right;"><a class="no-accent" href="/info" title="{{ translatableText "link.info" }}">v{{ info.Version }}</a></span></p> <span style="float:right;"><a class="no-accent" href="/info" title="{{ translatableText "link.info" }}">v{{ info.Version }}</a></span></p>
</footer> </footer>

BIN
public/favicon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 894 B

View file

@ -4,7 +4,7 @@
"description": "A plaintext diary", "description": "A plaintext diary",
"icons": [ "icons": [
{ {
"src": "/public/TODO.png", "src": "/public/favicon-512.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
}, },

30
public/themes/sans.css Normal file
View file

@ -0,0 +1,30 @@
/* Sans Undertale */
:root {
/* Light theme */
--text-light: #3c3836;
--bg-light: #e2e7e8;
--clickable-light: #41acda;
--clickable-hover-light: #2d8db4;
--clickable-label-light: #e2e8e2;
--text-hover-light: #665c54;
--textarea-bg-light: #f3f3f3;
--textarea-border-light: #3c3836;
/* Dark theme */
--text-dark: #e2e7e8;
--bg-dark: #25282a;
--clickable-dark: #41acda;
--clickable-hover-dark: #2d8db4;
--clickable-label-dark: #e2e8e2;
--text-hover-dark: #cdd2d3;
--textarea-bg-dark: #2d3234;
--textarea-border-dark: #3c3836;
}
body, textarea, input, button {
font-family: "Comic Sans MS", sans-serif;
}

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"embed"
"errors" "errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"html/template" "html/template"
@ -26,25 +27,40 @@ type Entry struct {
type formatEntries func([]string) []Entry type formatEntries func([]string) []Entry
//go:embed public
var Public embed.FS
//go:embed pages
var Pages embed.FS
// EmbeddedFile returns a file in Pages while "handling" potential errors
func EmbeddedFile(name string) []byte {
data, err := Pages.ReadFile(name)
if err != nil {
slog.Error("Error embedded file", "err", err)
}
return data
}
var templateFuncs = map[string]interface{}{ var templateFuncs = map[string]interface{}{
"translatableText": TranslatableText, "translatableText": TranslatableText,
"info": func() AppInfo { return Info }, "info": func() AppInfo { return Info },
"config": func() Config { return Cfg }, "config": func() Config { return Cfg },
} }
var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/edit.html")) var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/edit.html"))
var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/entry.html")) var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/entry.html"))
var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFiles("./pages/base.html", "./pages/list.html")) var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/list.html"))
// NotFound returns a user-friendly 404 error page // NotFound returns a user-friendly 404 error page
func NotFound(w http.ResponseWriter, r *http.Request) { func NotFound(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404) w.WriteHeader(404)
http.ServeFile(w, r, "./pages/error/404.html") HandleWrite(w.Write(EmbeddedFile("pages/error/404.html")))
} }
// InternalError returns a user-friendly 500 error page // InternalError returns a user-friendly 500 error page
func InternalError(w http.ResponseWriter, r *http.Request) { func InternalError(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500) w.WriteHeader(500)
http.ServeFile(w, r, "./pages/error/500.html") HandleWrite(w.Write(EmbeddedFile("pages/error/500.html")))
} }
// GetToday renders HTML page for today's entry // GetToday renders HTML page for today's entry
@ -97,7 +113,9 @@ func GetEntries(w http.ResponseWriter, r *http.Request, title string, descriptio
// GetDays renders HTML list of previous days' entries // GetDays renders HTML list of previous days' entries
func GetDays(w http.ResponseWriter, r *http.Request) { func GetDays(w http.ResponseWriter, r *http.Request) {
GetEntries(w, r, TranslatableText("title.days"), "", "day", func(files []string) []Entry { description := template.HTML(
"<a href=\"#footer\">" + TranslatableText("prompt.days") + "</a>")
GetEntries(w, r, TranslatableText("title.days"), description, "day", func(files []string) []Entry {
var filesFormatted []Entry var filesFormatted []Entry
for i := range files { for i := range files {
v := files[len(files)-1-i] // This is suboptimal, but reverse order is better here v := files[len(files)-1-i] // This is suboptimal, but reverse order is better here
@ -152,13 +170,6 @@ func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename str
} }
} }
files := []string{"./pages/base.html"}
if editable {
files = append(files, "./pages/edit.html")
} else {
files = append(files, "./pages/entry.html")
}
if editable { if editable {
err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: title, Content: string(entry)}) err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: title, Content: string(entry)})
} else { } else {

View file

@ -51,8 +51,8 @@ func Serve() {
r.Mount("/api", apiRouter) r.Mount("/api", apiRouter)
// Static files // Static files
fs := http.FileServer(http.Dir("public")) fs := http.FileServer(http.FS(Public))
r.Handle("/public/*", http.StripPrefix("/public/", fs)) r.Handle("/public/*", fs)
slog.Info("🌺 Website working", "port", Cfg.Port) slog.Info("🌺 Website working", "port", Cfg.Port)
slog.Debug("Debug mode enabled") slog.Debug("Debug mode enabled")