added color/ theme settings, new config window

This commit is contained in:
2026-02-20 14:43:35 -05:00
parent fa6506ed5d
commit 88c3736110
43 changed files with 2314 additions and 1207 deletions
-1
View File
@@ -65,7 +65,6 @@ func NewYellowJacketApp(
}
yjApp.appConfig = appConfig
yjApp.assetHandler.RegisterHandler("/config", yjApp.appConfig)
// create frontendUtil
feUtil, err := frontendutil.NewFrontendUtil()
-15
View File
@@ -1,15 +0,0 @@
package config
import "yellowjacket/pkg/templcomp"
templ (c *Config) form() {
@templcomp.ToForm(c, templ.URL("/config"), "config")
}
templ (c *Config) formSubmitError(msg string) {
<strong>Error: { msg }</strong>
}
templ (c *Config) formSubmitSuccess() {
<p>Config saved</p>
}
-113
View File
@@ -1,113 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package config
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "yellowjacket/pkg/templcomp"
func (c *Config) form() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templcomp.ToForm(c, templ.URL("/config"), "config").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func (c *Config) formSubmitError(msg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<strong>Error: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(msg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `config/config-form.templ`, Line: 10, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</strong>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func (c *Config) formSubmitSuccess() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p>Config saved</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
+120 -8
View File
@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"path"
@@ -16,17 +15,17 @@ import (
"yellowjacket/backend/events"
"yellowjacket/backend/library"
"yellowjacket/backend/system"
"yellowjacket/backend/theme"
)
// Config represents the application configuration.
type Config struct {
ctx context.Context
logger *slog.Logger
serveMux *http.ServeMux
filePath string // required
Library *library.Config `form:"Library" schema:"library,required"`
Window *WindowConfig `toml:"Window"`
Library *library.Config `toml:"Library"`
Theme *theme.Config `toml:"Theme"`
Window *WindowConfig `toml:"Window"`
}
// NewConfig creates a new config by loading it from disk.
@@ -38,11 +37,9 @@ func NewConfig(logger *slog.Logger) (*Config, error) {
conf := &Config{
filePath: path.Join(confDir, "config.toml"),
serveMux: http.NewServeMux(),
}
conf.applyDefaults()
conf.logger = logger.WithGroup("config").With("config", conf)
conf.serveMux.HandleFunc("/", conf.handle)
if err := conf.Load(); err != nil {
return nil, fmt.Errorf("could not load config: %w", err)
@@ -67,8 +64,17 @@ func (c *Config) Validate() error {
}
}
if c.Theme != nil {
if err := c.Theme.Validate(); err != nil {
configErrs = errors.Join(configErrs, err)
}
}
if configErrs != nil {
return fmt.Errorf("one or more config parts are invalid: %w", configErrs)
return fmt.Errorf(
"one or more config parts are invalid: %w",
configErrs,
)
}
return nil
@@ -148,6 +154,12 @@ func (c *Config) applyDefaults() {
if c.Library != nil {
c.Library.ApplyDefaults()
}
if c.Theme == nil {
c.Theme = &theme.Config{}
}
c.Theme.ApplyDefaults()
}
// SetContext sets the Wails runtime context for event emission.
@@ -245,3 +257,103 @@ func (c *Config) SetScanConcurrency(mode string) error {
return nil
}
// GetThemeAccentColor returns the configured accent colour.
func (c *Config) GetThemeAccentColor() string {
if c.Theme == nil {
return theme.DefaultAccentColor
}
return c.Theme.AccentColor
}
// GetThemeBackgroundShade returns the configured background shade.
func (c *Config) GetThemeBackgroundShade() string {
if c.Theme == nil {
return string(theme.DefaultBackgroundShade)
}
return string(c.Theme.BackgroundShade)
}
// SetThemeAccentColor validates and saves a new accent colour.
func (c *Config) SetThemeAccentColor(
color string,
) error {
if c.Theme == nil {
c.Theme = &theme.Config{}
c.Theme.ApplyDefaults()
}
c.Theme.AccentColor = color
if err := c.Theme.Validate(); err != nil {
return fmt.Errorf(
"invalid theme accent color: %w", err,
)
}
if err := c.Save(); err != nil {
return fmt.Errorf(
"could not save config: %w", err,
)
}
c.emitThemeChanged()
c.logger.Info(
"theme accent color updated",
"color", color,
)
return nil
}
// SetThemeBackgroundShade validates and saves a new background shade.
func (c *Config) SetThemeBackgroundShade(
shade string,
) error {
if c.Theme == nil {
c.Theme = &theme.Config{}
c.Theme.ApplyDefaults()
}
c.Theme.BackgroundShade = theme.BackgroundShade(shade)
if err := c.Theme.Validate(); err != nil {
return fmt.Errorf(
"invalid theme background shade: %w", err,
)
}
if err := c.Save(); err != nil {
return fmt.Errorf(
"could not save config: %w", err,
)
}
c.emitThemeChanged()
c.logger.Info(
"theme background shade updated",
"shade", shade,
)
return nil
}
// emitThemeChanged sends the ThemeConfigChanged event to the frontend.
func (c *Config) emitThemeChanged() {
if c.ctx == nil || c.Theme == nil {
return
}
runtime.EventsEmit(
c.ctx,
events.ThemeConfigChanged,
map[string]any{
"AccentColor": c.Theme.AccentColor,
"BackgroundShade": string(c.Theme.BackgroundShade),
},
)
}
-80
View File
@@ -1,80 +0,0 @@
package config
import (
"fmt"
"net/http"
"github.com/gorilla/schema"
"github.com/wailsapp/wails/v2/pkg/runtime"
"yellowjacket/backend/events"
)
var formDecoder = schema.NewDecoder()
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.serveMux.ServeHTTP(w, r)
}
func (c *Config) handle(w http.ResponseWriter, r *http.Request) {
c.logger.Debug("handling request from config http handler")
switch r.Method {
case http.MethodGet:
if err := c.form().Render(r.Context(), w); err != nil {
c.logger.Error("problem getting config html", "err", err.Error())
w.WriteHeader(http.StatusInternalServerError)
}
case http.MethodPost:
if err := c.handleConfigPost(r); err != nil {
c.logger.Error("problem handling config post request", "err", err.Error())
renderErr := c.formSubmitError(err.Error()).Render(r.Context(), w)
if renderErr != nil {
c.logger.Error("problem rendering error response", "err", renderErr.Error())
}
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := c.formSubmitSuccess().Render(r.Context(), w); err != nil {
c.logger.Error("problem rendering success response", "err", err.Error())
}
w.WriteHeader(http.StatusOK)
}
}
func (c *Config) handleConfigPost(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return fmt.Errorf("could not parse form data: %w", err)
}
var postedConfig Config
err := formDecoder.Decode(&postedConfig, r.PostForm)
if err != nil {
return fmt.Errorf("could not decode form data: %w", err)
}
c.logger.Debug("decoded config post form data", "postedConfig", postedConfig)
// Update local config and emit event for listeners
if postedConfig.Library != nil {
c.Library = postedConfig.Library
if c.ctx != nil {
runtime.EventsEmit(c.ctx, events.LibraryConfigChanged, map[string]any{
"DirectoryPath": string(c.Library.DirectoryPath),
})
}
}
if err := c.Save(); err != nil {
return fmt.Errorf("could not save posted config: %w", err)
}
return nil
}
+1
View File
@@ -55,6 +55,7 @@ const (
// Config events.
const (
LibraryConfigChanged = "LibraryConfigChanged"
ThemeConfigChanged = "ThemeConfigChanged"
)
// Playlist events.
-7
View File
@@ -1,7 +0,0 @@
package library
templ (d Directory) ToFormElement() {
<button type="button" onclick="selectLibraryDirectory(this.nextElementSibling)">Select</button>
<input type="text" name="library.directory" value={ d } readonly/>
<button type="button" onclick="scanLibrary(this)">Scan Library</button>
}
-53
View File
@@ -1,53 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package library
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func (d Directory) ToFormElement() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button type=\"button\" onclick=\"selectLibraryDirectory(this.nextElementSibling)\">Select</button> <input type=\"text\" name=\"library.directory\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(d)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library/config.templ`, Line: 5, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" readonly> <button type=\"button\" onclick=\"scanLibrary(this)\">Scan Library</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
+80
View File
@@ -0,0 +1,80 @@
// Package theme manages visual theme configuration.
package theme
import (
"errors"
"fmt"
"regexp"
)
var (
errInvalidHexColor = errors.New("invalid hex color")
errUnknownBackgroundShade = errors.New("unknown background shade")
)
// hexColorRe matches 3- or 6-digit CSS hex colours (e.g. "#fff", "#ffd43b").
var hexColorRe = regexp.MustCompile(`^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`)
// BackgroundShade controls the base grayscale palette.
type BackgroundShade string
// Valid BackgroundShade values.
const (
// BackgroundDarker is an OLED-friendly palette with true black.
BackgroundDarker BackgroundShade = "darker"
// BackgroundDark is the default dark palette.
BackgroundDark BackgroundShade = "dark"
// BackgroundLight is a light-mode palette.
BackgroundLight BackgroundShade = "light"
)
// Defaults.
const (
DefaultAccentColor = "#ffd43b"
DefaultBackgroundShade = BackgroundDark
)
// Config holds visual theme preferences.
type Config struct {
AccentColor string `toml:"AccentColor"`
BackgroundShade BackgroundShade `toml:"BackgroundShade"`
}
// ApplyDefaults fills zero-value fields with sensible defaults.
func (c *Config) ApplyDefaults() {
if c.AccentColor == "" {
c.AccentColor = DefaultAccentColor
}
if c.BackgroundShade == "" {
c.BackgroundShade = DefaultBackgroundShade
}
}
// Validate checks that all values are well-formed.
func (c *Config) Validate() error {
c.ApplyDefaults()
if !hexColorRe.MatchString(c.AccentColor) {
return fmt.Errorf(
"%w: %q",
errInvalidHexColor,
c.AccentColor,
)
}
switch c.BackgroundShade {
case BackgroundDarker, BackgroundDark, BackgroundLight:
// Valid.
default:
return fmt.Errorf(
"%w: %q",
errUnknownBackgroundShade,
c.BackgroundShade,
)
}
return nil
}