Move to embedded dirs
This commit is contained in:
parent
119ae9c3c6
commit
430a7461e6
16 changed files with 99 additions and 42 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
TODO.md
2
TODO.md
|
@ -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`,
|
||||||
|
|
|
@ -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
|
||||||
|
|
27
i18n.go
27
i18n.go
|
@ -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 {
|
||||||
|
loadLanguage := func(language string) error {
|
||||||
filename := "i18n/" + language + ".json"
|
filename := "i18n/" + language + ".json"
|
||||||
|
fileContents, err := I18n.ReadFile(filename)
|
||||||
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileContents, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error reading file",
|
slog.Error("error reading language file",
|
||||||
"error", err,
|
"error", err,
|
||||||
"file", filename)
|
"file", filename)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return json.Unmarshal(fileContents, &Translations)
|
||||||
err = json.Unmarshal(fileContents, &Translations)
|
}
|
||||||
|
err := loadLanguage("en") // Load english as fallback language
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return loadLanguage(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslatableText attempts to match an id to a string in current language
|
// TranslatableText attempts to match an id to a string in current language
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "исходный код",
|
||||||
|
|
4
info.go
4
info.go
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
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 |
|
@ -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
30
public/themes/sans.css
Normal 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;
|
||||||
|
}
|
37
routes.go
37
routes.go
|
@ -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 {
|
||||||
|
|
4
serve.go
4
serve.go
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue