added color/ theme settings, new config window
This commit is contained in:
@@ -65,7 +65,6 @@ func NewYellowJacketApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
yjApp.appConfig = appConfig
|
yjApp.appConfig = appConfig
|
||||||
yjApp.assetHandler.RegisterHandler("/config", yjApp.appConfig)
|
|
||||||
|
|
||||||
// create frontendUtil
|
// create frontendUtil
|
||||||
feUtil, err := frontendutil.NewFrontendUtil()
|
feUtil, err := frontendutil.NewFrontendUtil()
|
||||||
|
|||||||
@@ -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>
|
|
||||||
}
|
|
||||||
@@ -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
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
@@ -16,17 +15,17 @@ import (
|
|||||||
"yellowjacket/backend/events"
|
"yellowjacket/backend/events"
|
||||||
"yellowjacket/backend/library"
|
"yellowjacket/backend/library"
|
||||||
"yellowjacket/backend/system"
|
"yellowjacket/backend/system"
|
||||||
|
"yellowjacket/backend/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config represents the application configuration.
|
// Config represents the application configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
serveMux *http.ServeMux
|
|
||||||
filePath string // required
|
filePath string // required
|
||||||
Library *library.Config `form:"Library" schema:"library,required"`
|
Library *library.Config `toml:"Library"`
|
||||||
|
Theme *theme.Config `toml:"Theme"`
|
||||||
Window *WindowConfig `toml:"Window"`
|
Window *WindowConfig `toml:"Window"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a new config by loading it from disk.
|
// NewConfig creates a new config by loading it from disk.
|
||||||
@@ -38,11 +37,9 @@ func NewConfig(logger *slog.Logger) (*Config, error) {
|
|||||||
|
|
||||||
conf := &Config{
|
conf := &Config{
|
||||||
filePath: path.Join(confDir, "config.toml"),
|
filePath: path.Join(confDir, "config.toml"),
|
||||||
serveMux: http.NewServeMux(),
|
|
||||||
}
|
}
|
||||||
conf.applyDefaults()
|
conf.applyDefaults()
|
||||||
conf.logger = logger.WithGroup("config").With("config", conf)
|
conf.logger = logger.WithGroup("config").With("config", conf)
|
||||||
conf.serveMux.HandleFunc("/", conf.handle)
|
|
||||||
|
|
||||||
if err := conf.Load(); err != nil {
|
if err := conf.Load(); err != nil {
|
||||||
return nil, fmt.Errorf("could not load config: %w", err)
|
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 {
|
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
|
return nil
|
||||||
@@ -148,6 +154,12 @@ func (c *Config) applyDefaults() {
|
|||||||
if c.Library != nil {
|
if c.Library != nil {
|
||||||
c.Library.ApplyDefaults()
|
c.Library.ApplyDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Theme == nil {
|
||||||
|
c.Theme = &theme.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Theme.ApplyDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContext sets the Wails runtime context for event emission.
|
// SetContext sets the Wails runtime context for event emission.
|
||||||
@@ -245,3 +257,103 @@ func (c *Config) SetScanConcurrency(mode string) error {
|
|||||||
|
|
||||||
return nil
|
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),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -55,6 +55,7 @@ const (
|
|||||||
// Config events.
|
// Config events.
|
||||||
const (
|
const (
|
||||||
LibraryConfigChanged = "LibraryConfigChanged"
|
LibraryConfigChanged = "LibraryConfigChanged"
|
||||||
|
ThemeConfigChanged = "ThemeConfigChanged"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Playlist events.
|
// Playlist events.
|
||||||
|
|||||||
@@ -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>
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+10
-10
@@ -10,8 +10,8 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: black;
|
background-color: var(--yj-bg-base, black);
|
||||||
color: white;
|
color: var(--yj-text-primary, white);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -32,7 +32,7 @@ p {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@@ -51,14 +51,14 @@ ul {
|
|||||||
|
|
||||||
body div.sidebar {
|
body div.sidebar {
|
||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
background-color: #212529;
|
background-color: var(--yj-bg-surface, #212529);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-bar {
|
.bottom-bar {
|
||||||
grid-area: bottom-bar;
|
grid-area: bottom-bar;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--now-playing-width, 200px) 1fr auto;
|
grid-template-columns: var(--now-playing-width, 200px) 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -75,7 +75,7 @@ body div.sidebar {
|
|||||||
height: 3.5em;
|
height: 3.5em;
|
||||||
min-height: 3.5em;
|
min-height: 3.5em;
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
background-color: #ffd43b;
|
background-color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
#track-info {
|
#track-info {
|
||||||
@@ -114,12 +114,12 @@ body div.sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#queue-button:hover {
|
#queue-button:hover {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
#queue-button.drag-over {
|
#queue-button.drag-over {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
outline: 2px dashed #ffd43b;
|
outline: 2px dashed var(--yj-accent, #ffd43b);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ body div.sidebar {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
background-color: #212529;
|
background-color: var(--yj-bg-surface, #212529);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,6 @@
|
|||||||
<h1 class="title">YellowJacket</h1>
|
<h1 class="title">YellowJacket</h1>
|
||||||
<h3 class="subtitle">Music how it was meant to bee.</h3>
|
<h3 class="subtitle">Music how it was meant to bee.</h3>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
<a href="/src/pages/config/">
|
|
||||||
<img src="/src/assets/images/icons/ui/settings.svg" />
|
|
||||||
</a>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<app-sidebar></app-sidebar>
|
<app-sidebar></app-sidebar>
|
||||||
|
|||||||
+8
-1
@@ -6,12 +6,16 @@ import '@components/sidebar/app-sidebar.ts';
|
|||||||
import '@components/queue-panel/queue-panel.ts';
|
import '@components/queue-panel/queue-panel.ts';
|
||||||
import '@components/playlist-view/playlist-view.ts';
|
import '@components/playlist-view/playlist-view.ts';
|
||||||
import '@components/library-manager/library-manager.ts';
|
import '@components/library-manager/library-manager.ts';
|
||||||
|
import '@components/config-page/config-page.ts';
|
||||||
import '@awesome.me/webawesome/dist/styles/themes/default.css';
|
import '@awesome.me/webawesome/dist/styles/themes/default.css';
|
||||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||||
import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js';
|
import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js';
|
||||||
import { libraryStore } from '@store/library-store';
|
import { libraryStore } from '@store/library-store';
|
||||||
import { playlistStore } from '@store/playlist-store';
|
import { playlistStore } from '@store/playlist-store';
|
||||||
import { queueStore } from '@store/queue-store';
|
import { queueStore } from '@store/queue-store';
|
||||||
|
// Importing the theme store triggers initialization: it fetches the saved
|
||||||
|
// theme from the backend and applies CSS custom properties to :root.
|
||||||
|
import '@store/theme-store';
|
||||||
import {
|
import {
|
||||||
hasTrackPayload,
|
hasTrackPayload,
|
||||||
getDragPayload,
|
getDragPayload,
|
||||||
@@ -46,8 +50,11 @@ document.addEventListener('navigate', (e: Event) => {
|
|||||||
case 'libraries':
|
case 'libraries':
|
||||||
mainContent.innerHTML = '<library-manager></library-manager>';
|
mainContent.innerHTML = '<library-manager></library-manager>';
|
||||||
break;
|
break;
|
||||||
|
case 'settings':
|
||||||
|
mainContent.innerHTML = '<config-page></config-page>';
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
mainContent.innerHTML = `<div style="padding: 1em; color: #b3b3b3;">
|
mainContent.innerHTML = `<div style="padding: 1em; color: var(--yj-text-secondary, #b3b3b3);">
|
||||||
<p>Coming soon: ${view}</p>
|
<p>Coming soon: ${view}</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"@awesome.me/webawesome": "^3.2.1",
|
"@awesome.me/webawesome": "^3.2.1",
|
||||||
"@lit-labs/signals": "^0.2.0",
|
"@lit-labs/signals": "^0.2.0",
|
||||||
"@lit-labs/virtualizer": "^2.1.1",
|
"@lit-labs/virtualizer": "^2.1.1",
|
||||||
"htmx.org": "2.0.8",
|
|
||||||
"lit": "^3.2.1"
|
"lit": "^3.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ export class PlayerControls extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repeat-one {
|
.repeat-one {
|
||||||
|
|||||||
@@ -22,23 +22,23 @@ export class SeekBar extends LitElement {
|
|||||||
--track-size: 6px;
|
--track-size: 6px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0 1em;
|
margin: 0 1em;
|
||||||
--wa-tooltip-background-color: #343a40;
|
--wa-tooltip-background-color: var(--yj-bg-elevated, #343a40);
|
||||||
--wa-tooltip-content-color: white;
|
--wa-tooltip-content-color: var(--yj-text-primary, white);
|
||||||
--wa-tooltip-border-color: #343a40;
|
--wa-tooltip-border-color: var(--yj-bg-elevated, #343a40);
|
||||||
--wa-tooltip-border-radius: 4px;
|
--wa-tooltip-border-radius: 4px;
|
||||||
--wa-tooltip-font-size: 0.875em;
|
--wa-tooltip-font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-slider::part(track) {
|
wa-slider::part(track) {
|
||||||
background: white;
|
background: var(--yj-text-primary, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-slider::part(indicator) {
|
wa-slider::part(indicator) {
|
||||||
background: yellow;
|
background: var(--yj-accent, yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-slider::part(thumb) {
|
wa-slider::part(thumb) {
|
||||||
background: black;
|
background: var(--yj-bg-base, black);
|
||||||
}
|
}
|
||||||
|
|
||||||
#seek-bar-container {
|
#seek-bar-container {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ export class VolumeControl extends LitElement {
|
|||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background: #1a1a1a;
|
background: var(--yj-bg-surface, #1a1a1a);
|
||||||
border: 1px solid #333;
|
border: 1px solid var(--yj-border-subtle, #333);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1em 0.5em;
|
padding: 1em 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
@@ -52,16 +52,16 @@ export class VolumeControl extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wa-slider::part(track) {
|
wa-slider::part(track) {
|
||||||
background: white;
|
background: var(--yj-text-primary, white);
|
||||||
height: 120px;
|
height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-slider::part(indicator) {
|
wa-slider::part(indicator) {
|
||||||
background: yellow;
|
background: var(--yj-accent, yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-slider::part(thumb) {
|
wa-slider::part(thumb) {
|
||||||
background: black;
|
background: var(--yj-bg-base, black);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
import { LitElement, html, css, nothing } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema describing a single config field.
|
||||||
|
*
|
||||||
|
* The `type` controls which input widget is rendered:
|
||||||
|
* - `text` – plain text input
|
||||||
|
* - `select` – dropdown with `options`
|
||||||
|
* - `color` – colour picker swatch
|
||||||
|
* - `directory` – readonly text + browse button
|
||||||
|
* - `number` – numeric input
|
||||||
|
* - `toggle` – on/off switch
|
||||||
|
*/
|
||||||
|
export interface ConfigFieldSchema {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
type:
|
||||||
|
| 'text'
|
||||||
|
| 'select'
|
||||||
|
| 'color'
|
||||||
|
| 'directory'
|
||||||
|
| 'number'
|
||||||
|
| 'toggle';
|
||||||
|
options?: { value: string; label: string }[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigFieldChangeEvent {
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('config-field')
|
||||||
|
export class ConfigField extends LitElement {
|
||||||
|
static override styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
color-scheme: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--yj-text-tertiary, #888);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
input[type='number'] {
|
||||||
|
background: var(--yj-bg-elevated, #343a40);
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
border: 1px solid var(--yj-border-subtle, #333);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-family: inherit;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: 1px solid var(--yj-accent, #ffd43b);
|
||||||
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[readonly] {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: var(--yj-bg-elevated, #343a40);
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
border: 1px solid var(--yj-border-subtle, #333);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
outline: 1px solid var(--yj-accent, #ffd43b);
|
||||||
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
|
}
|
||||||
|
|
||||||
|
select option {
|
||||||
|
background: var(--yj-bg-elevated, #343a40);
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--yj-info, #4263eb);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--yj-info-hover, #3b5bdb);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colour picker */
|
||||||
|
.color-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color'] {
|
||||||
|
width: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
border: 2px solid var(--yj-border, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color']::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color']::-webkit-color-swatch {
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-hex {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle */
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 2.5em;
|
||||||
|
height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--yj-bg-overlay, #495057);
|
||||||
|
border-radius: 1em;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
left: 0.2em;
|
||||||
|
bottom: 0.2em;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:checked + .toggle-slider {
|
||||||
|
background: var(--yj-accent, #ffd43b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:checked + .toggle-slider::before {
|
||||||
|
transform: translateX(1.1em);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
schema!: ConfigFieldSchema;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
value: unknown = '';
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
if (!this.schema) return nothing;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="field">
|
||||||
|
${this.schema.type === 'toggle'
|
||||||
|
? this.renderToggle()
|
||||||
|
: html`
|
||||||
|
<label>${this.schema.label}</label>
|
||||||
|
${this.renderInput()}
|
||||||
|
`}
|
||||||
|
${this.schema.description
|
||||||
|
? html`<p class="description">
|
||||||
|
${this.schema.description}
|
||||||
|
</p>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderInput() {
|
||||||
|
switch (this.schema.type) {
|
||||||
|
case 'text':
|
||||||
|
return this.renderText();
|
||||||
|
case 'number':
|
||||||
|
return this.renderNumber();
|
||||||
|
case 'select':
|
||||||
|
return this.renderSelect();
|
||||||
|
case 'color':
|
||||||
|
return this.renderColor();
|
||||||
|
case 'directory':
|
||||||
|
return this.renderDirectory();
|
||||||
|
default:
|
||||||
|
return html`<p>Unsupported field type</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderText() {
|
||||||
|
return html`
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
.value=${String(this.value ?? '')}
|
||||||
|
?disabled=${this.schema.disabled}
|
||||||
|
@change=${this.onTextChange}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderNumber() {
|
||||||
|
return html`
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
.value=${String(this.value ?? '')}
|
||||||
|
?disabled=${this.schema.disabled}
|
||||||
|
@change=${this.onTextChange}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSelect() {
|
||||||
|
const current = String(this.value ?? '');
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<select
|
||||||
|
?disabled=${this.schema.disabled}
|
||||||
|
@change=${this.onSelectChange}
|
||||||
|
>
|
||||||
|
${(this.schema.options ?? []).map(
|
||||||
|
(opt) => html`
|
||||||
|
<option
|
||||||
|
value=${opt.value}
|
||||||
|
?selected=${opt.value === current}
|
||||||
|
>
|
||||||
|
${opt.label}
|
||||||
|
</option>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderColor() {
|
||||||
|
const hex = String(this.value ?? '#ffffff');
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="color-wrapper">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
.value=${hex}
|
||||||
|
?disabled=${this.schema.disabled}
|
||||||
|
@input=${this.onColorInput}
|
||||||
|
/>
|
||||||
|
<span class="color-hex">${hex}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDirectory() {
|
||||||
|
return html`
|
||||||
|
<div class="input-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
.value=${String(this.value ?? '')}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
?disabled=${this.schema.disabled}
|
||||||
|
@click=${this.onBrowseClick}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderToggle() {
|
||||||
|
const checked = Boolean(this.value);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="toggle-row">
|
||||||
|
<label>${this.schema.label}</label>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
?checked=${checked}
|
||||||
|
?disabled=${this.schema.disabled}
|
||||||
|
@change=${this.onToggleChange}
|
||||||
|
/>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// EVENT DISPATCHERS
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
private emitChange(value: unknown): void {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent<ConfigFieldChangeEvent>(
|
||||||
|
'config-change',
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
key: this.schema.key,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTextChange = (e: Event) => {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
this.emitChange(input.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSelectChange = (e: Event) => {
|
||||||
|
const select = e.target as HTMLSelectElement;
|
||||||
|
this.emitChange(select.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onColorInput = (e: Event) => {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
this.value = input.value;
|
||||||
|
this.emitChange(input.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onBrowseClick = () => {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('config-browse', {
|
||||||
|
detail: { key: this.schema.key },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onToggleChange = (e: Event) => {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
this.emitChange(input.checked);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'config-field': ConfigField;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
import { LitElement, html, css, nothing } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A visual grouping wrapper for config fields.
|
||||||
|
* Renders a heading, optional description, and a slot for fields.
|
||||||
|
*/
|
||||||
|
@customElement('config-section')
|
||||||
|
export class ConfigSection extends LitElement {
|
||||||
|
static override styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: var(--yj-bg-surface, #212529);
|
||||||
|
border: 1px solid var(--yj-border-subtle, #333);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 0.25em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--yj-text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--yj-text-tertiary, #888);
|
||||||
|
margin: 0 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
heading = '';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
description = '';
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<div class="section">
|
||||||
|
<h3>${this.heading}</h3>
|
||||||
|
${this.description
|
||||||
|
? html`<p class="description">
|
||||||
|
${this.description}
|
||||||
|
</p>`
|
||||||
|
: nothing}
|
||||||
|
<div class="fields">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'config-section': ConfigSection;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ export class AlbumDropdown extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.album-dropdown {
|
.album-dropdown {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
border-radius: 0 0 4px 4px;
|
border-radius: 0 0 4px 4px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -81,7 +81,7 @@ export class AlbumDropdown extends LitElement {
|
|||||||
height: 0;
|
height: 0;
|
||||||
border-left: 9px solid transparent;
|
border-left: 9px solid transparent;
|
||||||
border-right: 9px solid transparent;
|
border-right: 9px solid transparent;
|
||||||
border-bottom: 8px solid #343a40;
|
border-bottom: 8px solid var(--yj-bg-elevated, #343a40);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-tracks {
|
.dropdown-tracks {
|
||||||
@@ -103,11 +103,9 @@ export class AlbumDropdown extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-row:hover {
|
.track-row:hover {
|
||||||
background-color: rgba(
|
background-color: var(
|
||||||
255,
|
--yj-hover-overlay,
|
||||||
255,
|
rgba(255, 255, 255, 0.05)
|
||||||
255,
|
|
||||||
0.05
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,13 +119,11 @@ export class AlbumDropdown extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-row.active {
|
.track-row.active {
|
||||||
background-color: rgba(
|
background-color: var(
|
||||||
255,
|
--yj-accent-bg,
|
||||||
212,
|
rgba(255, 212, 59, 0.1)
|
||||||
59,
|
|
||||||
0.1
|
|
||||||
);
|
);
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-row.selected.active {
|
.track-row.selected.active {
|
||||||
@@ -140,14 +136,14 @@ export class AlbumDropdown extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-number {
|
.track-number {
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
min-width: 22px;
|
min-width: 22px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-title {
|
.track-title {
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -156,7 +152,7 @@ export class AlbumDropdown extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-duration {
|
.track-duration {
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,21 +203,21 @@ export class CoverGrid extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.album-card:hover {
|
.album-card:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-card.selected {
|
.album-card.selected {
|
||||||
outline: 2px solid #ffd43b;
|
outline: 2px solid var(--yj-accent, #ffd43b);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-card.expanded {
|
.album-card.expanded {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-card:focus-visible {
|
.album-card:focus-visible {
|
||||||
outline: 2px solid #ffd43b;
|
outline: 2px solid var(--yj-accent, #ffd43b);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ export class CoverGrid extends LitElement {
|
|||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #282828;
|
background-color: var(--yj-bg-surface, #282828);
|
||||||
transition: scale 0.15s ease;
|
transition: scale 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,8 +263,8 @@ export class CoverGrid extends LitElement {
|
|||||||
|
|
||||||
.select-toggle.checked {
|
.select-toggle.checked {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: #ffd43b;
|
background-color: var(--yj-accent, #ffd43b);
|
||||||
border-color: #ffd43b;
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-icon {
|
.check-icon {
|
||||||
@@ -289,10 +289,10 @@ export class CoverGrid extends LitElement {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
#404040 0%,
|
var(--yj-bg-overlay, #404040) 0%,
|
||||||
#282828 100%
|
var(--yj-bg-surface, #282828) 100%
|
||||||
);
|
);
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
font-size: var(--placeholder-font, 48px);
|
font-size: var(--placeholder-font, 48px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ export class CoverGrid extends LitElement {
|
|||||||
.album-name {
|
.album-name {
|
||||||
font-size: var(--album-name-font, 14px);
|
font-size: var(--album-name-font, 14px);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -318,7 +318,7 @@ export class CoverGrid extends LitElement {
|
|||||||
|
|
||||||
.artist-name {
|
.artist-name {
|
||||||
font-size: var(--artist-name-font, 12px);
|
font-size: var(--artist-name-font, 12px);
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -326,7 +326,7 @@ export class CoverGrid extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.album-year {
|
.album-year {
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
@@ -338,7 +338,7 @@ export class CoverGrid extends LitElement {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
@@ -347,7 +347,7 @@ export class CoverGrid extends LitElement {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 48px;
|
padding: 48px;
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,8 +364,8 @@ export class CoverGrid extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel {
|
.context-menu-panel {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
border: 1px solid #444;
|
border: 1px solid var(--yj-border, #444);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
@@ -377,16 +377,14 @@ export class CoverGrid extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel wa-dropdown-item {
|
.context-menu-panel wa-dropdown-item {
|
||||||
--wa-color-text-normal: #fff;
|
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel wa-dropdown-item:hover {
|
.context-menu-panel wa-dropdown-item:hover {
|
||||||
background-color: rgba(
|
background-color: var(
|
||||||
255,
|
--yj-hover-overlay,
|
||||||
255,
|
rgba(255, 255, 255, 0.1)
|
||||||
255,
|
|
||||||
0.1
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export class LibraryManager extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
color: #e9ecef;
|
color: var(--yj-text-primary, #e9ecef);
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -251,13 +251,13 @@ export class LibraryManager extends LitElement {
|
|||||||
margin: 0 0 1em 0;
|
margin: 0 0 1em 0;
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #f8f9fa;
|
color: var(--yj-text-primary, #f8f9fa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
padding: 1.25em;
|
padding: 1.25em;
|
||||||
background: #2b3035;
|
background: var(--yj-bg-surface, #2b3035);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ export class LibraryManager extends LitElement {
|
|||||||
margin: 0 0 0.75em 0;
|
margin: 0 0 0.75em 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #dee2e6;
|
color: var(--yj-text-primary, #dee2e6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
@@ -282,7 +282,7 @@ export class LibraryManager extends LitElement {
|
|||||||
.section-description {
|
.section-description {
|
||||||
margin: 0 0 1em 0;
|
margin: 0 0 1em 0;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #868e96;
|
color: var(--yj-text-tertiary, #868e96);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,10 +296,10 @@ export class LibraryManager extends LitElement {
|
|||||||
.directory-path {
|
.directory-path {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.5em 0.75em;
|
padding: 0.5em 0.75em;
|
||||||
background: #1a1d20;
|
background: var(--yj-bg-elevated, #1a1d20);
|
||||||
border: 1px solid #495057;
|
border: 1px solid var(--yj-bg-overlay, #495057);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #adb5bd;
|
color: var(--yj-text-secondary, #adb5bd);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -309,7 +309,7 @@ export class LibraryManager extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.directory-path.has-value {
|
.directory-path.has-value {
|
||||||
color: #e9ecef;
|
color: var(--yj-text-primary, #e9ecef);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -329,57 +329,57 @@ export class LibraryManager extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: #4263eb;
|
background: var(--yj-info, #4263eb);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
.btn-primary:hover:not(:disabled) {
|
||||||
background: #3b5bdb;
|
background: var(--yj-info-hover, #3b5bdb);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-success {
|
.btn-success {
|
||||||
background: #2f9e44;
|
background: var(--yj-success, #2f9e44);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-success:hover:not(:disabled) {
|
.btn-success:hover:not(:disabled) {
|
||||||
background: #2b8a3e;
|
background: var(--yj-success-hover, #2b8a3e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning {
|
.btn-warning {
|
||||||
background: #e8590c;
|
background: var(--yj-warning, #e8590c);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning:hover:not(:disabled) {
|
.btn-warning:hover:not(:disabled) {
|
||||||
background: #d9480f;
|
background: var(--yj-warning-hover, #d9480f);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: #e03131;
|
background: var(--yj-error, #e03131);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
.btn-danger:hover:not(:disabled) {
|
||||||
background: #c92a2a;
|
background: var(--yj-error-hover, #c92a2a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #868e96;
|
color: var(--yj-text-tertiary, #868e96);
|
||||||
padding: 0.3em 0.75em;
|
padding: 0.3em 0.75em;
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
border: 1px solid #495057;
|
border: 1px solid var(--yj-bg-overlay, #495057);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost:hover:not(:disabled) {
|
.btn-ghost:hover:not(:disabled) {
|
||||||
background: #495057;
|
background: var(--yj-bg-overlay, #495057);
|
||||||
color: #e9ecef;
|
color: var(--yj-text-primary, #e9ecef);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost.copied {
|
.btn-ghost.copied {
|
||||||
border-color: #2f9e44;
|
border-color: var(--yj-success, #2f9e44);
|
||||||
color: #2f9e44;
|
color: var(--yj-success, #2f9e44);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-row {
|
.setting-row {
|
||||||
@@ -390,16 +390,16 @@ export class LibraryManager extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setting-row label {
|
.setting-row label {
|
||||||
color: #adb5bd;
|
color: var(--yj-text-secondary, #adb5bd);
|
||||||
min-width: 8em;
|
min-width: 8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-row select {
|
.setting-row select {
|
||||||
padding: 0.4em 0.6em;
|
padding: 0.4em 0.6em;
|
||||||
background: #1a1d20;
|
background: var(--yj-bg-elevated, #1a1d20);
|
||||||
border: 1px solid #495057;
|
border: 1px solid var(--yj-bg-overlay, #495057);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #e9ecef;
|
color: var(--yj-text-primary, #e9ecef);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -408,12 +408,12 @@ export class LibraryManager extends LitElement {
|
|||||||
|
|
||||||
.setting-row select:focus {
|
.setting-row select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #4263eb;
|
border-color: var(--yj-info, #4263eb);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-row select option {
|
.setting-row select option {
|
||||||
background: #2b3035;
|
background: var(--yj-bg-surface, #2b3035);
|
||||||
color: #e9ecef;
|
color: var(--yj-text-primary, #e9ecef);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-actions {
|
.scan-actions {
|
||||||
@@ -425,15 +425,15 @@ export class LibraryManager extends LitElement {
|
|||||||
.status-bar {
|
.status-bar {
|
||||||
margin-top: 1.5em;
|
margin-top: 1.5em;
|
||||||
padding: 0.75em 1em;
|
padding: 0.75em 1em;
|
||||||
background: #1a1d20;
|
background: var(--yj-bg-elevated, #1a1d20);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #868e96;
|
color: var(--yj-text-tertiary, #868e96);
|
||||||
min-height: 1.2em;
|
min-height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar.active {
|
.status-bar.active {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Metrics tree --- */
|
/* --- Metrics tree --- */
|
||||||
@@ -453,7 +453,7 @@ export class LibraryManager extends LitElement {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.25em 0;
|
padding: 0.25em 0;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #ced4da;
|
color: var(--yj-text-secondary, #ced4da);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,21 +484,21 @@ export class LibraryManager extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.metric-label {
|
.metric-label {
|
||||||
color: #adb5bd;
|
color: var(--yj-text-secondary, #adb5bd);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-value {
|
.metric-value {
|
||||||
color: #e9ecef;
|
color: var(--yj-text-primary, #e9ecef);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-value.highlight {
|
.metric-value.highlight {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-note {
|
.metric-note {
|
||||||
color: #868e96;
|
color: var(--yj-text-tertiary, #868e96);
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding-left: 1.35em;
|
padding-left: 1.35em;
|
||||||
@@ -513,11 +513,11 @@ export class LibraryManager extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.count-label {
|
.count-label {
|
||||||
color: #adb5bd;
|
color: var(--yj-text-secondary, #adb5bd);
|
||||||
}
|
}
|
||||||
|
|
||||||
.count-value {
|
.count-value {
|
||||||
color: #e9ecef;
|
color: var(--yj-text-primary, #e9ecef);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ export class NowPlaying extends LitElement {
|
|||||||
.cover-placeholder {
|
.cover-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #000;
|
background-color: var(--yj-bg-base, #000);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-placeholder wa-icon {
|
.cover-placeholder wa-icon {
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ export class NowPlaying extends LitElement {
|
|||||||
|
|
||||||
.track-artist {
|
.track-artist {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: var(--yj-text-tertiary, #666);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -96,7 +96,7 @@ export class NowPlaying extends LitElement {
|
|||||||
|
|
||||||
.resize-handle:hover,
|
.resize-handle:hover,
|
||||||
.resize-handle.dragging {
|
.resize-handle.dragging {
|
||||||
background-color: #6c757d;
|
background-color: var(--yj-text-tertiary, #6c757d);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export class PlaylistPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.picker-panel {
|
.picker-panel {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
border: 1px solid #444;
|
border: 1px solid var(--yj-border, #444);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
@@ -49,17 +49,17 @@ export class PlaylistPicker extends LitElement {
|
|||||||
|
|
||||||
.picker-panel wa-dropdown-item {
|
.picker-panel wa-dropdown-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
--wa-color-text-normal: #fff;
|
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-panel wa-dropdown-item:hover {
|
.picker-panel wa-dropdown-item:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: #555;
|
background: var(--yj-border-subtle, #555);
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +71,10 @@ export class PlaylistPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-form input {
|
.create-form input {
|
||||||
background: #2a2d30;
|
background: var(--yj-bg-surface, #2b3035);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--yj-border-subtle, #555);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -82,11 +82,11 @@ export class PlaylistPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-form input:focus {
|
.create-form input:focus {
|
||||||
border-color: #ffd43b;
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form input::placeholder {
|
.create-form input::placeholder {
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
@@ -96,10 +96,10 @@ export class PlaylistPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-row button {
|
.button-row button {
|
||||||
background: #495057;
|
background: var(--yj-bg-overlay, #495057);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -111,23 +111,23 @@ export class PlaylistPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-row button.primary {
|
.button-row button.primary {
|
||||||
background: #ffd43b;
|
background: var(--yj-accent, #ffd43b);
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row button.primary:hover {
|
.button-row button.primary:hover {
|
||||||
background: #ffe066;
|
background: var(--yj-accent-hover, #ffe066);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row button.primary:disabled {
|
.button-row button.primary:disabled {
|
||||||
background: #665a1e;
|
background: var(--yj-accent-muted, #665a1e);
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-message {
|
.empty-message {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -205,21 +205,21 @@ export class PlaylistView
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h2 {
|
.header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-playlist-button {
|
.new-playlist-button {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--yj-border-subtle, #555);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -230,8 +230,8 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.new-playlist-button:hover {
|
.new-playlist-button:hover {
|
||||||
border-color: #ffd43b;
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form {
|
.create-form {
|
||||||
@@ -239,16 +239,16 @@ export class PlaylistView
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form input {
|
.create-form input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #2a2d30;
|
background: var(--yj-bg-surface, #2b3035);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--yj-border-subtle, #555);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -256,18 +256,18 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-form input:focus {
|
.create-form input:focus {
|
||||||
border-color: #ffd43b;
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form input::placeholder {
|
.create-form input::placeholder {
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form button {
|
.create-form button {
|
||||||
background: #495057;
|
background: var(--yj-bg-overlay, #495057);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -275,21 +275,21 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-form button:hover {
|
.create-form button:hover {
|
||||||
background: #5a6268;
|
background: var(--yj-bg-overlay, #5a6268);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form button.primary {
|
.create-form button.primary {
|
||||||
background: #ffd43b;
|
background: var(--yj-accent, #ffd43b);
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form button.primary:hover {
|
.create-form button.primary:hover {
|
||||||
background: #ffe066;
|
background: var(--yj-accent-hover, #ffe066);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-form button.primary:disabled {
|
.create-form button.primary:disabled {
|
||||||
background: #665a1e;
|
background: var(--yj-accent-muted, #665a1e);
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +303,7 @@ export class PlaylistView
|
|||||||
|
|
||||||
.playlist-item {
|
.playlist-item {
|
||||||
border-bottom: 1px solid
|
border-bottom: 1px solid
|
||||||
rgba(255, 255, 255, 0.05);
|
var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist-header {
|
.playlist-header {
|
||||||
@@ -316,18 +316,18 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.playlist-header:hover {
|
.playlist-header:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist-item.drag-over > .playlist-header {
|
.playlist-item.drag-over > .playlist-header {
|
||||||
background-color: rgba(255, 212, 59, 0.15);
|
background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
|
||||||
outline: 1px dashed #ffd43b;
|
outline: 1px dashed var(--yj-accent, #ffd43b);
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
}
|
}
|
||||||
@@ -338,13 +338,13 @@ export class PlaylistView
|
|||||||
|
|
||||||
.playlist-icon {
|
.playlist-icon {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist-name {
|
.playlist-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -353,7 +353,7 @@ export class PlaylistView
|
|||||||
|
|
||||||
.track-count {
|
.track-count {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #666;
|
color: var(--yj-text-tertiary, #666);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +370,9 @@ export class PlaylistView
|
|||||||
|
|
||||||
.play-all-button {
|
.play-all-button {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--yj-border-subtle, #555);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -383,8 +383,8 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.play-all-button:hover {
|
.play-all-button:hover {
|
||||||
border-color: #ffd43b;
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item {
|
.track-item {
|
||||||
@@ -396,20 +396,20 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-item:hover {
|
.track-item:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.selected {
|
.track-item.selected {
|
||||||
background-color: rgba(100, 160, 255, 0.15);
|
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.active {
|
.track-item.active {
|
||||||
background-color: rgba(255, 212, 59, 0.1);
|
background-color: var(--yj-accent-bg, rgba(255, 212, 59, 0.1));
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.selected.active {
|
.track-item.selected.active {
|
||||||
background-color: rgba(100, 160, 255, 0.15);
|
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.phantom {
|
.track-item.phantom {
|
||||||
@@ -424,7 +424,7 @@ export class PlaylistView
|
|||||||
.phantom-badge {
|
.phantom-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #e67700;
|
color: var(--yj-warning, #e67700);
|
||||||
background: rgba(230, 119, 0, 0.15);
|
background: rgba(230, 119, 0, 0.15);
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@@ -438,7 +438,7 @@ export class PlaylistView
|
|||||||
|
|
||||||
.tracks-empty {
|
.tracks-empty {
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
color: #666;
|
color: var(--yj-text-tertiary, #666);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ export class PlaylistView
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
@@ -456,7 +456,7 @@ export class PlaylistView
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 48px 20px;
|
padding: 48px 20px;
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@@ -474,8 +474,8 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel {
|
.context-menu-panel {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
border: 1px solid #444;
|
border: 1px solid var(--yj-border, #444);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
@@ -484,7 +484,7 @@ export class PlaylistView
|
|||||||
|
|
||||||
.context-menu-panel wa-dropdown-item {
|
.context-menu-panel wa-dropdown-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
--wa-color-text-normal: #fff;
|
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,10 +512,10 @@ export class PlaylistView
|
|||||||
|
|
||||||
.rename-input {
|
.rename-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #2a2d30;
|
background: var(--yj-bg-surface, #2b3035);
|
||||||
border: 1px solid #ffd43b;
|
border: 1px solid var(--yj-accent, #ffd43b);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -525,9 +525,9 @@ export class PlaylistView
|
|||||||
|
|
||||||
.import-button {
|
.import-button {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--yj-border-subtle, #555);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -538,8 +538,8 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
|
|
||||||
.import-button:hover {
|
.import-button:hover {
|
||||||
border-color: #ffd43b;
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export class QueuePanel
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #212529;
|
background-color: var(--yj-bg-surface, #212529);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ export class QueuePanel
|
|||||||
--queue-width,
|
--queue-width,
|
||||||
${unsafeCSS(DEFAULT_WIDTH)}px
|
${unsafeCSS(DEFAULT_WIDTH)}px
|
||||||
);
|
);
|
||||||
border-left: 1px solid #333;
|
border-left: 1px solid var(--yj-border-subtle, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
@@ -180,7 +180,7 @@ export class QueuePanel
|
|||||||
|
|
||||||
.resize-handle:hover,
|
.resize-handle:hover,
|
||||||
.resize-handle.dragging {
|
.resize-handle.dragging {
|
||||||
background-color: #6c757d;
|
background-color: var(--yj-text-tertiary, #6c757d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-content {
|
.panel-content {
|
||||||
@@ -196,7 +196,7 @@ export class QueuePanel
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,11 +223,11 @@ export class QueuePanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-action-button:hover {
|
.header-action-button:hover {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-action-button:disabled {
|
.header-action-button:disabled {
|
||||||
color: #555;
|
color: var(--yj-border-subtle, #555);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ export class QueuePanel
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
border-bottom: 1px solid
|
border-bottom: 1px solid
|
||||||
rgba(255, 255, 255, 0.05);
|
var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -255,30 +255,30 @@ export class QueuePanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-item:hover {
|
.track-item:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.selected {
|
.track-item.selected {
|
||||||
background-color: rgba(100, 160, 255, 0.15);
|
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.active {
|
.track-item.active {
|
||||||
background-color: rgba(255, 212, 59, 0.1);
|
background-color: var(--yj-accent-bg, rgba(255, 212, 59, 0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.selected.active {
|
.track-item.selected.active {
|
||||||
background-color: rgba(100, 160, 255, 0.15);
|
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-position {
|
.track-position {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item.active .track-position {
|
.track-item.active .track-position {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-details {
|
.track-details {
|
||||||
@@ -297,12 +297,12 @@ export class QueuePanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-item.active .track-title {
|
.track-item.active .track-title {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-artist {
|
.track-artist {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -311,7 +311,7 @@ export class QueuePanel
|
|||||||
.remove-button {
|
.remove-button {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -325,11 +325,11 @@ export class QueuePanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remove-button:hover {
|
.remove-button:hover {
|
||||||
color: #ff6b6b;
|
color: var(--yj-error, #ff6b6b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-content.drag-over {
|
.panel-content.drag-over {
|
||||||
outline: 2px dashed #ffd43b;
|
outline: 2px dashed var(--yj-accent, #ffd43b);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ export class QueuePanel
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
border-bottom: 1px solid
|
border-bottom: 1px solid
|
||||||
rgba(255, 212, 59, 0.2);
|
rgba(255, 212, 59, 0.2);
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ export class QueuePanel
|
|||||||
left: 8px;
|
left: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: #ffd43b;
|
background: var(--yj-accent, #ffd43b);
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -368,7 +368,7 @@ export class QueuePanel
|
|||||||
left: 8px;
|
left: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: #ffd43b;
|
background: var(--yj-accent, #ffd43b);
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -380,7 +380,7 @@ export class QueuePanel
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
color: #b3b3b3;
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@@ -394,8 +394,8 @@ export class QueuePanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel {
|
.context-menu-panel {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
border: 1px solid #444;
|
border: 1px solid var(--yj-border, #444);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
@@ -404,7 +404,7 @@ export class QueuePanel
|
|||||||
|
|
||||||
.context-menu-panel wa-dropdown-item {
|
.context-menu-panel wa-dropdown-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
--wa-color-text-normal: #fff;
|
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { LitElement, html, css } from 'lit';
|
import { LitElement, html, css } from 'lit';
|
||||||
import { customElement, state } from 'lit/decorators.js';
|
import { customElement, state } from 'lit/decorators.js';
|
||||||
|
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||||
|
|
||||||
import type { DragActiveDetail } from '@utils/drag-controller';
|
import type { DragActiveDetail } from '@utils/drag-controller';
|
||||||
|
|
||||||
type View = 'home' | 'libraries' | 'playlists' | 'artists' | 'albums' | 'tracks';
|
type View = 'home' | 'libraries' | 'playlists' | 'artists' | 'albums' | 'tracks' | 'settings';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
id: View;
|
id: View;
|
||||||
label: string;
|
label: string;
|
||||||
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_WIDTH = 120;
|
const MIN_WIDTH = 120;
|
||||||
@@ -21,7 +23,7 @@ export class AppSidebar extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #212529;
|
background-color: var(--yj-bg-surface, #212529);
|
||||||
min-width: ${MIN_WIDTH}px;
|
min-width: ${MIN_WIDTH}px;
|
||||||
max-width: ${MAX_WIDTH}px;
|
max-width: ${MAX_WIDTH}px;
|
||||||
}
|
}
|
||||||
@@ -40,7 +42,7 @@ export class AppSidebar extends LitElement {
|
|||||||
|
|
||||||
.resize-handle:hover,
|
.resize-handle:hover,
|
||||||
.resize-handle.dragging {
|
.resize-handle.dragging {
|
||||||
background-color: #6c757d;
|
background-color: var(--yj-text-tertiary, #6c757d);
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@@ -50,19 +52,28 @@ export class AppSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
text-align: left;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li wa-icon {
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
li:hover {
|
li:hover {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
}
|
}
|
||||||
|
|
||||||
li.active {
|
li.active {
|
||||||
background-color: #495057;
|
background-color: var(--yj-bg-overlay, #495057);
|
||||||
}
|
}
|
||||||
|
|
||||||
li p {
|
li p {
|
||||||
@@ -73,8 +84,8 @@ export class AppSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li.drag-hover {
|
li.drag-hover {
|
||||||
background-color: rgba(255, 212, 59, 0.15);
|
background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
|
||||||
outline: 1px dashed #ffd43b;
|
outline: 1px dashed var(--yj-accent, #ffd43b);
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -101,12 +112,13 @@ export class AppSidebar extends LitElement {
|
|||||||
> | null = null;
|
> | null = null;
|
||||||
|
|
||||||
private navItems: NavItem[] = [
|
private navItems: NavItem[] = [
|
||||||
{ id: 'home', label: 'Home' },
|
{ id: 'home', label: 'Home', icon: 'house' },
|
||||||
{ id: 'libraries', label: 'Libraries' },
|
{ id: 'libraries', label: 'Libraries', icon: 'folder-open' },
|
||||||
{ id: 'playlists', label: 'Playlists' },
|
{ id: 'playlists', label: 'Playlists', icon: 'list' },
|
||||||
{ id: 'artists', label: 'Artists' },
|
{ id: 'artists', label: 'Artists', icon: 'user-group' },
|
||||||
{ id: 'albums', label: 'Albums' },
|
{ id: 'albums', label: 'Albums', icon: 'compact-disc' },
|
||||||
{ id: 'tracks', label: 'Tracks' },
|
{ id: 'tracks', label: 'Tracks', icon: 'music' },
|
||||||
|
{ id: 'settings', label: 'Settings', icon: 'gear' },
|
||||||
];
|
];
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
@@ -179,6 +191,9 @@ export class AppSidebar extends LitElement {
|
|||||||
@drop=${(e: DragEvent) =>
|
@drop=${(e: DragEvent) =>
|
||||||
this.onNavDrop(e)}
|
this.onNavDrop(e)}
|
||||||
>
|
>
|
||||||
|
<wa-icon
|
||||||
|
name=${item.icon}
|
||||||
|
></wa-icon>
|
||||||
<p>${item.label}</p>
|
<p>${item.label}</p>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -63,14 +63,14 @@ export class TrackInfo extends LitElement {
|
|||||||
.cover-placeholder {
|
.cover-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #2a2d30;
|
background-color: var(--yj-bg-elevated, #2a2d30);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-placeholder wa-icon {
|
.cover-placeholder wa-icon {
|
||||||
color: #666;
|
color: var(--yj-text-tertiary, #666);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export class TrackInfo extends LitElement {
|
|||||||
.title {
|
.title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -93,7 +93,7 @@ export class TrackInfo extends LitElement {
|
|||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -101,7 +101,7 @@ export class TrackInfo extends LitElement {
|
|||||||
|
|
||||||
.duration {
|
.duration {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #888;
|
color: var(--yj-text-tertiary, #888);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,8 +252,8 @@ export class TrackList extends LitElement implements SelectionHost {
|
|||||||
grid-template-columns: var(--grid-cols, 1fr 1fr 80px);
|
grid-template-columns: var(--grid-cols, 1fr 1fr 80px);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #fff;
|
color: var(--yj-text-primary, #fff);
|
||||||
border-bottom: 1px solid #666;
|
border-bottom: 1px solid var(--yj-text-tertiary, #666);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ export class TrackList extends LitElement implements SelectionHost {
|
|||||||
width: 1px;
|
width: 1px;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
background-color: #444;
|
background-color: var(--yj-border, #444);
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@ export class TrackList extends LitElement implements SelectionHost {
|
|||||||
|
|
||||||
.col-resize-handle:hover,
|
.col-resize-handle:hover,
|
||||||
.col-resize-handle.active {
|
.col-resize-handle.active {
|
||||||
background-color: #6c757d;
|
background-color: var(--yj-text-tertiary, #6c757d);
|
||||||
}
|
}
|
||||||
|
|
||||||
lit-virtualizer {
|
lit-virtualizer {
|
||||||
@@ -307,7 +307,7 @@ export class TrackList extends LitElement implements SelectionHost {
|
|||||||
grid-template-columns: var(--grid-cols, 1fr 1fr 80px);
|
grid-template-columns: var(--grid-cols, 1fr 1fr 80px);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -324,23 +324,23 @@ export class TrackList extends LitElement implements SelectionHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-row:hover {
|
.track-row:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-row.selected {
|
.track-row.selected {
|
||||||
background-color: rgba(100, 160, 255, 0.15);
|
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-row.active {
|
.track-row.active {
|
||||||
background-color: rgba(255, 212, 59, 0.1);
|
background-color: var(--yj-accent-bg, rgba(255, 212, 59, 0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-row.active {
|
.track-row.active {
|
||||||
color: #ffd43b;
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-row.selected.active {
|
.track-row.selected.active {
|
||||||
background-color: rgba(100, 160, 255, 0.15);
|
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-name {
|
.track-name {
|
||||||
@@ -363,8 +363,8 @@ export class TrackList extends LitElement implements SelectionHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel {
|
.context-menu-panel {
|
||||||
background-color: #343a40;
|
background-color: var(--yj-bg-elevated, #343a40);
|
||||||
border: 1px solid #444;
|
border: 1px solid var(--yj-border, #444);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
@@ -376,12 +376,12 @@ export class TrackList extends LitElement implements SelectionHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel wa-dropdown-item {
|
.context-menu-panel wa-dropdown-item {
|
||||||
--wa-color-text-normal: #fff;
|
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-panel wa-dropdown-item:hover {
|
.context-menu-panel wa-dropdown-item:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.submenu-item {
|
.submenu-item {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export const Events = {
|
|||||||
PlaylistTracksChanged: "PlaylistTracksChanged",
|
PlaylistTracksChanged: "PlaylistTracksChanged",
|
||||||
PlaylistsRestored: "PlaylistsRestored",
|
PlaylistsRestored: "PlaylistsRestored",
|
||||||
|
|
||||||
|
// Config events
|
||||||
|
ThemeConfigChanged: "ThemeConfigChanged",
|
||||||
|
|
||||||
// Library events
|
// Library events
|
||||||
LibraryScanStarted: "LibraryScanStarted",
|
LibraryScanStarted: "LibraryScanStarted",
|
||||||
LibraryScanComplete: "LibraryScanComplete",
|
LibraryScanComplete: "LibraryScanComplete",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import 'htmx.org/dist/htmx.js'
|
|
||||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
|
||||||
import { Scan } from '@go/library/Library';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
DirectoryPicker: typeof DirectoryPicker;
|
|
||||||
Scan: typeof Scan;
|
|
||||||
selectLibraryDirectory: (pElement: HTMLInputElement) => void;
|
|
||||||
scanLibrary: (button: HTMLButtonElement) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.DirectoryPicker = DirectoryPicker;
|
|
||||||
window.Scan = Scan;
|
|
||||||
|
|
||||||
window.selectLibraryDirectory = (pElement: HTMLInputElement) => {
|
|
||||||
window.DirectoryPicker()
|
|
||||||
.then((result) => {
|
|
||||||
if (result.length !== 0) {
|
|
||||||
pElement.value = result;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
console.error('error with directory picker: ' + err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.scanLibrary = (button: HTMLButtonElement) => {
|
|
||||||
button.disabled = true;
|
|
||||||
button.textContent = 'Scanning...';
|
|
||||||
window.Scan()
|
|
||||||
.then(() => {
|
|
||||||
button.textContent = 'Scan Library';
|
|
||||||
button.disabled = false;
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
console.error('error scanning library: ' + err);
|
|
||||||
button.textContent = 'Scan Library';
|
|
||||||
button.disabled = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
|
||||||
<link href="./config.css" rel="stylesheet" />
|
|
||||||
<title>yellowjacket - config</title>
|
|
||||||
</head>
|
|
||||||
<script src="./config.ts" type="module"></script>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="container">
|
|
||||||
<hgroup>
|
|
||||||
<a href="/index.html">
|
|
||||||
<h1>YellowJacket</h1>
|
|
||||||
</a>
|
|
||||||
</hgroup>
|
|
||||||
</header>
|
|
||||||
<hr />
|
|
||||||
<main class="container">
|
|
||||||
<div hx-get="/config" hx-trigger="load"></div>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||||
|
import type { ThemeState, BackgroundShade } from '../theme-store';
|
||||||
|
import { themeStore } from '../theme-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeController connects a Lit component to the ThemeStore.
|
||||||
|
*
|
||||||
|
* Most components do not need this because CSS custom properties
|
||||||
|
* cascade into Shadow DOM automatically. Use this controller only
|
||||||
|
* in components that need to read or change theme values (e.g. the
|
||||||
|
* config page colour picker).
|
||||||
|
*/
|
||||||
|
export class ThemeController implements ReactiveController {
|
||||||
|
private host: ReactiveControllerHost;
|
||||||
|
private unsubscribe?: () => void;
|
||||||
|
|
||||||
|
constructor(host: ReactiveControllerHost) {
|
||||||
|
this.host = host;
|
||||||
|
host.addController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// LIFECYCLE HOOKS
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
hostConnected(): void {
|
||||||
|
this.unsubscribe = themeStore.subscribe(() => {
|
||||||
|
this.host.requestUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDisconnected(): void {
|
||||||
|
this.unsubscribe?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// STATE ACCESSORS
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
get state(): Readonly<ThemeState> {
|
||||||
|
return themeStore.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
get accentColor(): string {
|
||||||
|
return this.state.accentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
get backgroundShade(): BackgroundShade {
|
||||||
|
return this.state.backgroundShade;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// ACTIONS
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
async setAccentColor(color: string): Promise<void> {
|
||||||
|
await themeStore.setAccentColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBackgroundShade(
|
||||||
|
shade: BackgroundShade,
|
||||||
|
): Promise<void> {
|
||||||
|
await themeStore.setBackgroundShade(shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,3 +4,6 @@ export { PlayerController } from './controllers/player-controller';
|
|||||||
export { queueStore } from './queue-store';
|
export { queueStore } from './queue-store';
|
||||||
export type { QueueState, QueueTrack, RepeatMode } from './queue-store';
|
export type { QueueState, QueueTrack, RepeatMode } from './queue-store';
|
||||||
export { QueueController } from './controllers/queue-controller';
|
export { QueueController } from './controllers/queue-controller';
|
||||||
|
export { themeStore } from './theme-store';
|
||||||
|
export type { ThemeState, BackgroundShade } from './theme-store';
|
||||||
|
export { ThemeController } from './controllers/theme-controller';
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import { EventsOn } from '@runtime/runtime';
|
||||||
|
import {
|
||||||
|
GetThemeAccentColor,
|
||||||
|
GetThemeBackgroundShade,
|
||||||
|
SetThemeAccentColor,
|
||||||
|
SetThemeBackgroundShade,
|
||||||
|
} from '@go/config/Config';
|
||||||
|
import { Events } from '../events';
|
||||||
|
|
||||||
|
export type BackgroundShade = 'darker' | 'dark' | 'light';
|
||||||
|
|
||||||
|
export interface ThemeState {
|
||||||
|
accentColor: string;
|
||||||
|
backgroundShade: BackgroundShade;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscriber = () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shade palettes keyed by BackgroundShade.
|
||||||
|
* Each defines the base grayscale ramp used throughout the UI.
|
||||||
|
*/
|
||||||
|
interface ShadePalette {
|
||||||
|
bgBase: string;
|
||||||
|
bgSurface: string;
|
||||||
|
bgElevated: string;
|
||||||
|
bgOverlay: string;
|
||||||
|
textPrimary: string;
|
||||||
|
textSecondary: string;
|
||||||
|
textTertiary: string;
|
||||||
|
border: string;
|
||||||
|
borderSubtle: string;
|
||||||
|
hoverOverlay: string;
|
||||||
|
selectionBg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHADE_PALETTES: Record<BackgroundShade, ShadePalette> = {
|
||||||
|
darker: {
|
||||||
|
bgBase: '#000000',
|
||||||
|
bgSurface: '#121212',
|
||||||
|
bgElevated: '#1e1e1e',
|
||||||
|
bgOverlay: '#2a2a2a',
|
||||||
|
textPrimary: '#ffffff',
|
||||||
|
textSecondary: '#b3b3b3',
|
||||||
|
textTertiary: '#888888',
|
||||||
|
border: '#333333',
|
||||||
|
borderSubtle: '#222222',
|
||||||
|
hoverOverlay: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
selectionBg: 'rgba(100, 160, 255, 0.15)',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
bgBase: '#000000',
|
||||||
|
bgSurface: '#212529',
|
||||||
|
bgElevated: '#343a40',
|
||||||
|
bgOverlay: '#495057',
|
||||||
|
textPrimary: '#ffffff',
|
||||||
|
textSecondary: '#b3b3b3',
|
||||||
|
textTertiary: '#888888',
|
||||||
|
border: '#444444',
|
||||||
|
borderSubtle: '#333333',
|
||||||
|
hoverOverlay: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
selectionBg: 'rgba(100, 160, 255, 0.15)',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
bgBase: '#ffffff',
|
||||||
|
bgSurface: '#f8f9fa',
|
||||||
|
bgElevated: '#e9ecef',
|
||||||
|
bgOverlay: '#dee2e6',
|
||||||
|
textPrimary: '#212529',
|
||||||
|
textSecondary: '#495057',
|
||||||
|
textTertiary: '#868e96',
|
||||||
|
border: '#ced4da',
|
||||||
|
borderSubtle: '#dee2e6',
|
||||||
|
hoverOverlay: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
selectionBg: 'rgba(100, 160, 255, 0.15)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parse a hex colour string to RGB components. */
|
||||||
|
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||||
|
let cleaned = hex.replace('#', '');
|
||||||
|
|
||||||
|
if (cleaned.length === 3) {
|
||||||
|
cleaned =
|
||||||
|
cleaned[0]! + cleaned[0]! +
|
||||||
|
cleaned[1]! + cleaned[1]! +
|
||||||
|
cleaned[2]! + cleaned[2]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: parseInt(cleaned.slice(0, 2), 16),
|
||||||
|
g: parseInt(cleaned.slice(2, 4), 16),
|
||||||
|
b: parseInt(cleaned.slice(4, 6), 16),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mix a colour towards white by a fraction (0..1). */
|
||||||
|
function lighten(hex: string, amount: number): string {
|
||||||
|
const { r, g, b } = hexToRgb(hex);
|
||||||
|
const lr = Math.round(r + (255 - r) * amount);
|
||||||
|
const lg = Math.round(g + (255 - g) * amount);
|
||||||
|
const lb = Math.round(b + (255 - b) * amount);
|
||||||
|
|
||||||
|
return `#${lr.toString(16).padStart(2, '0')}${lg.toString(16).padStart(2, '0')}${lb.toString(16).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mix a colour towards black by a fraction (0..1). */
|
||||||
|
function darken(hex: string, amount: number): string {
|
||||||
|
const { r, g, b } = hexToRgb(hex);
|
||||||
|
const dr = Math.round(r * (1 - amount));
|
||||||
|
const dg = Math.round(g * (1 - amount));
|
||||||
|
const db = Math.round(b * (1 - amount));
|
||||||
|
|
||||||
|
return `#${dr.toString(16).padStart(2, '0')}${dg.toString(16).padStart(2, '0')}${db.toString(16).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the full set of CSS custom properties from accent + shade.
|
||||||
|
*/
|
||||||
|
function deriveThemeVariables(
|
||||||
|
accent: string,
|
||||||
|
shade: BackgroundShade,
|
||||||
|
): Record<string, string> {
|
||||||
|
const palette = SHADE_PALETTES[shade];
|
||||||
|
const { r, g, b } = hexToRgb(accent);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Background ramp
|
||||||
|
'--yj-bg-base': palette.bgBase,
|
||||||
|
'--yj-bg-surface': palette.bgSurface,
|
||||||
|
'--yj-bg-elevated': palette.bgElevated,
|
||||||
|
'--yj-bg-overlay': palette.bgOverlay,
|
||||||
|
|
||||||
|
// Accent ramp
|
||||||
|
'--yj-accent': accent,
|
||||||
|
'--yj-accent-hover': lighten(accent, 0.15),
|
||||||
|
'--yj-accent-muted': darken(accent, 0.5),
|
||||||
|
'--yj-accent-bg': `rgba(${r}, ${g}, ${b}, 0.1)`,
|
||||||
|
'--yj-accent-bg-strong': `rgba(${r}, ${g}, ${b}, 0.15)`,
|
||||||
|
|
||||||
|
// Text
|
||||||
|
'--yj-text-primary': palette.textPrimary,
|
||||||
|
'--yj-text-secondary': palette.textSecondary,
|
||||||
|
'--yj-text-tertiary': palette.textTertiary,
|
||||||
|
|
||||||
|
// Borders
|
||||||
|
'--yj-border': palette.border,
|
||||||
|
'--yj-border-subtle': palette.borderSubtle,
|
||||||
|
|
||||||
|
// Interactive overlays
|
||||||
|
'--yj-hover-overlay': palette.hoverOverlay,
|
||||||
|
'--yj-selection-bg': palette.selectionBg,
|
||||||
|
|
||||||
|
// Semantic colours (fixed across themes)
|
||||||
|
'--yj-success': '#2f9e44',
|
||||||
|
'--yj-success-hover': '#2b8a3e',
|
||||||
|
'--yj-warning': '#e8590c',
|
||||||
|
'--yj-warning-hover': '#d9480f',
|
||||||
|
'--yj-error': '#e03131',
|
||||||
|
'--yj-error-hover': '#c92a2a',
|
||||||
|
'--yj-info': '#4263eb',
|
||||||
|
'--yj-info-hover': '#3b5bdb',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThemeStore {
|
||||||
|
private state: ThemeState = {
|
||||||
|
accentColor: '#ffd43b',
|
||||||
|
backgroundShade: 'dark',
|
||||||
|
};
|
||||||
|
|
||||||
|
private subscribers = new Set<Subscriber>();
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeEventListeners();
|
||||||
|
this.loadFromBackend();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// WAILS EVENT BRIDGE
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
private initializeEventListeners(): void {
|
||||||
|
EventsOn(
|
||||||
|
Events.ThemeConfigChanged,
|
||||||
|
(data: {
|
||||||
|
AccentColor: string;
|
||||||
|
BackgroundShade: string;
|
||||||
|
}) => {
|
||||||
|
this.update({
|
||||||
|
accentColor: data.AccentColor,
|
||||||
|
backgroundShade:
|
||||||
|
data.BackgroundShade as BackgroundShade,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadFromBackend(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const [accent, shade] = await Promise.all([
|
||||||
|
GetThemeAccentColor(),
|
||||||
|
GetThemeBackgroundShade(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.update({
|
||||||
|
accentColor: accent,
|
||||||
|
backgroundShade: shade as BackgroundShade,
|
||||||
|
});
|
||||||
|
this.initialized = true;
|
||||||
|
} catch {
|
||||||
|
// Use defaults on failure, apply them so the UI has variables.
|
||||||
|
this.applyVariables();
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// STATE ACCESS
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
getState(): Readonly<ThemeState> {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized(): boolean {
|
||||||
|
return this.initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// ACTIONS
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
async setAccentColor(color: string): Promise<void> {
|
||||||
|
await SetThemeAccentColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBackgroundShade(
|
||||||
|
shade: BackgroundShade,
|
||||||
|
): Promise<void> {
|
||||||
|
await SetThemeBackgroundShade(shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// SUBSCRIPTION SYSTEM
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
subscribe(callback: Subscriber): () => void {
|
||||||
|
this.subscribers.add(callback);
|
||||||
|
|
||||||
|
return () => this.subscribers.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(partial: Partial<ThemeState>): void {
|
||||||
|
this.state = { ...this.state, ...partial };
|
||||||
|
this.applyVariables();
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify(): void {
|
||||||
|
this.subscribers.forEach((callback) => callback());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the full set of CSS custom properties to :root so every
|
||||||
|
* component (including Shadow DOM) inherits them automatically.
|
||||||
|
*/
|
||||||
|
private applyVariables(): void {
|
||||||
|
const vars = deriveThemeVariables(
|
||||||
|
this.state.accentColor,
|
||||||
|
this.state.backgroundShade,
|
||||||
|
);
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
for (const [prop, value] of Object.entries(vars)) {
|
||||||
|
root.style.setProperty(prop, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the document color-scheme so native form controls
|
||||||
|
// (select dropdowns, scrollbars, etc.) match the theme.
|
||||||
|
root.style.colorScheme =
|
||||||
|
this.state.backgroundShade === 'light'
|
||||||
|
? 'light'
|
||||||
|
: 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance.
|
||||||
|
export const themeStore = new ThemeStore();
|
||||||
@@ -17,7 +17,6 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
main: "index.html",
|
main: "index.html",
|
||||||
config: "src/pages/config/index.html",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+8
-3
@@ -1,22 +1,27 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {http} from '../models';
|
|
||||||
import {context} from '../models';
|
import {context} from '../models';
|
||||||
|
|
||||||
export function GetLibraryDirectory():Promise<string>;
|
export function GetLibraryDirectory():Promise<string>;
|
||||||
|
|
||||||
export function GetScanConcurrency():Promise<string>;
|
export function GetScanConcurrency():Promise<string>;
|
||||||
|
|
||||||
|
export function GetThemeAccentColor():Promise<string>;
|
||||||
|
|
||||||
|
export function GetThemeBackgroundShade():Promise<string>;
|
||||||
|
|
||||||
export function Load():Promise<void>;
|
export function Load():Promise<void>;
|
||||||
|
|
||||||
export function Save():Promise<void>;
|
export function Save():Promise<void>;
|
||||||
|
|
||||||
export function ServeHTTP(arg1:http.ResponseWriter,arg2:http.Request):Promise<void>;
|
|
||||||
|
|
||||||
export function SetContext(arg1:context.Context):Promise<void>;
|
export function SetContext(arg1:context.Context):Promise<void>;
|
||||||
|
|
||||||
export function SetLibraryDirectory(arg1:string):Promise<void>;
|
export function SetLibraryDirectory(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function SetScanConcurrency(arg1:string):Promise<void>;
|
export function SetScanConcurrency(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SetThemeAccentColor(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SetThemeBackgroundShade(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function Validate():Promise<void>;
|
export function Validate():Promise<void>;
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ export function GetScanConcurrency() {
|
|||||||
return window['go']['config']['Config']['GetScanConcurrency']();
|
return window['go']['config']['Config']['GetScanConcurrency']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetThemeAccentColor() {
|
||||||
|
return window['go']['config']['Config']['GetThemeAccentColor']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetThemeBackgroundShade() {
|
||||||
|
return window['go']['config']['Config']['GetThemeBackgroundShade']();
|
||||||
|
}
|
||||||
|
|
||||||
export function Load() {
|
export function Load() {
|
||||||
return window['go']['config']['Config']['Load']();
|
return window['go']['config']['Config']['Load']();
|
||||||
}
|
}
|
||||||
@@ -18,10 +26,6 @@ export function Save() {
|
|||||||
return window['go']['config']['Config']['Save']();
|
return window['go']['config']['Config']['Save']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServeHTTP(arg1, arg2) {
|
|
||||||
return window['go']['config']['Config']['ServeHTTP'](arg1, arg2);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SetContext(arg1) {
|
export function SetContext(arg1) {
|
||||||
return window['go']['config']['Config']['SetContext'](arg1);
|
return window['go']['config']['Config']['SetContext'](arg1);
|
||||||
}
|
}
|
||||||
@@ -34,6 +38,14 @@ export function SetScanConcurrency(arg1) {
|
|||||||
return window['go']['config']['Config']['SetScanConcurrency'](arg1);
|
return window['go']['config']['Config']['SetScanConcurrency'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SetThemeAccentColor(arg1) {
|
||||||
|
return window['go']['config']['Config']['SetThemeAccentColor'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SetThemeBackgroundShade(arg1) {
|
||||||
|
return window['go']['config']['Config']['SetThemeBackgroundShade'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function Validate() {
|
export function Validate() {
|
||||||
return window['go']['config']['Config']['Validate']();
|
return window['go']['config']['Config']['Validate']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,132 +1,3 @@
|
|||||||
export namespace http {
|
|
||||||
|
|
||||||
export class Response {
|
|
||||||
Status: string;
|
|
||||||
StatusCode: number;
|
|
||||||
Proto: string;
|
|
||||||
ProtoMajor: number;
|
|
||||||
ProtoMinor: number;
|
|
||||||
Header: Record<string, Array<string>>;
|
|
||||||
Body: any;
|
|
||||||
ContentLength: number;
|
|
||||||
TransferEncoding: string[];
|
|
||||||
Close: boolean;
|
|
||||||
Uncompressed: boolean;
|
|
||||||
Trailer: Record<string, Array<string>>;
|
|
||||||
Request?: Request;
|
|
||||||
TLS?: tls.ConnectionState;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Response(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Status = source["Status"];
|
|
||||||
this.StatusCode = source["StatusCode"];
|
|
||||||
this.Proto = source["Proto"];
|
|
||||||
this.ProtoMajor = source["ProtoMajor"];
|
|
||||||
this.ProtoMinor = source["ProtoMinor"];
|
|
||||||
this.Header = source["Header"];
|
|
||||||
this.Body = source["Body"];
|
|
||||||
this.ContentLength = source["ContentLength"];
|
|
||||||
this.TransferEncoding = source["TransferEncoding"];
|
|
||||||
this.Close = source["Close"];
|
|
||||||
this.Uncompressed = source["Uncompressed"];
|
|
||||||
this.Trailer = source["Trailer"];
|
|
||||||
this.Request = this.convertValues(source["Request"], Request);
|
|
||||||
this.TLS = this.convertValues(source["TLS"], tls.ConnectionState);
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class Request {
|
|
||||||
Method: string;
|
|
||||||
URL?: url.URL;
|
|
||||||
Proto: string;
|
|
||||||
ProtoMajor: number;
|
|
||||||
ProtoMinor: number;
|
|
||||||
Header: Record<string, Array<string>>;
|
|
||||||
Body: any;
|
|
||||||
ContentLength: number;
|
|
||||||
TransferEncoding: string[];
|
|
||||||
Close: boolean;
|
|
||||||
Host: string;
|
|
||||||
Form: Record<string, Array<string>>;
|
|
||||||
PostForm: Record<string, Array<string>>;
|
|
||||||
MultipartForm?: multipart.Form;
|
|
||||||
Trailer: Record<string, Array<string>>;
|
|
||||||
RemoteAddr: string;
|
|
||||||
RequestURI: string;
|
|
||||||
TLS?: tls.ConnectionState;
|
|
||||||
Response?: Response;
|
|
||||||
Pattern: string;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Request(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Method = source["Method"];
|
|
||||||
this.URL = this.convertValues(source["URL"], url.URL);
|
|
||||||
this.Proto = source["Proto"];
|
|
||||||
this.ProtoMajor = source["ProtoMajor"];
|
|
||||||
this.ProtoMinor = source["ProtoMinor"];
|
|
||||||
this.Header = source["Header"];
|
|
||||||
this.Body = source["Body"];
|
|
||||||
this.ContentLength = source["ContentLength"];
|
|
||||||
this.TransferEncoding = source["TransferEncoding"];
|
|
||||||
this.Close = source["Close"];
|
|
||||||
this.Host = source["Host"];
|
|
||||||
this.Form = source["Form"];
|
|
||||||
this.PostForm = source["PostForm"];
|
|
||||||
this.MultipartForm = this.convertValues(source["MultipartForm"], multipart.Form);
|
|
||||||
this.Trailer = source["Trailer"];
|
|
||||||
this.RemoteAddr = source["RemoteAddr"];
|
|
||||||
this.RequestURI = source["RequestURI"];
|
|
||||||
this.TLS = this.convertValues(source["TLS"], tls.ConnectionState);
|
|
||||||
this.Response = this.convertValues(source["Response"], Response);
|
|
||||||
this.Pattern = source["Pattern"];
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace library {
|
export namespace library {
|
||||||
|
|
||||||
export class Album {
|
export class Album {
|
||||||
@@ -240,163 +111,6 @@ export namespace library {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace multipart {
|
|
||||||
|
|
||||||
export class FileHeader {
|
|
||||||
Filename: string;
|
|
||||||
Header: Record<string, Array<string>>;
|
|
||||||
Size: number;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new FileHeader(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Filename = source["Filename"];
|
|
||||||
this.Header = source["Header"];
|
|
||||||
this.Size = source["Size"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class Form {
|
|
||||||
Value: Record<string, Array<string>>;
|
|
||||||
File: Record<string, Array<FileHeader>>;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Form(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Value = source["Value"];
|
|
||||||
this.File = this.convertValues(source["File"], Array<FileHeader>, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace net {
|
|
||||||
|
|
||||||
export class IPNet {
|
|
||||||
IP: number[];
|
|
||||||
Mask: number[];
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new IPNet(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.IP = source["IP"];
|
|
||||||
this.Mask = source["Mask"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace pkix {
|
|
||||||
|
|
||||||
export class AttributeTypeAndValue {
|
|
||||||
Type: number[];
|
|
||||||
Value: any;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new AttributeTypeAndValue(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Type = source["Type"];
|
|
||||||
this.Value = source["Value"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class Extension {
|
|
||||||
Id: number[];
|
|
||||||
Critical: boolean;
|
|
||||||
Value: number[];
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Extension(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Id = source["Id"];
|
|
||||||
this.Critical = source["Critical"];
|
|
||||||
this.Value = source["Value"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class Name {
|
|
||||||
Country: string[];
|
|
||||||
Organization: string[];
|
|
||||||
OrganizationalUnit: string[];
|
|
||||||
Locality: string[];
|
|
||||||
Province: string[];
|
|
||||||
StreetAddress: string[];
|
|
||||||
PostalCode: string[];
|
|
||||||
SerialNumber: string;
|
|
||||||
CommonName: string;
|
|
||||||
Names: AttributeTypeAndValue[];
|
|
||||||
ExtraNames: AttributeTypeAndValue[];
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Name(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Country = source["Country"];
|
|
||||||
this.Organization = source["Organization"];
|
|
||||||
this.OrganizationalUnit = source["OrganizationalUnit"];
|
|
||||||
this.Locality = source["Locality"];
|
|
||||||
this.Province = source["Province"];
|
|
||||||
this.StreetAddress = source["StreetAddress"];
|
|
||||||
this.PostalCode = source["PostalCode"];
|
|
||||||
this.SerialNumber = source["SerialNumber"];
|
|
||||||
this.CommonName = source["CommonName"];
|
|
||||||
this.Names = this.convertValues(source["Names"], AttributeTypeAndValue);
|
|
||||||
this.ExtraNames = this.convertValues(source["ExtraNames"], AttributeTypeAndValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace playlist {
|
export namespace playlist {
|
||||||
|
|
||||||
export class Summary {
|
export class Summary {
|
||||||
@@ -482,318 +196,3 @@ export namespace playlist {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace tls {
|
|
||||||
|
|
||||||
export class ConnectionState {
|
|
||||||
Version: number;
|
|
||||||
HandshakeComplete: boolean;
|
|
||||||
DidResume: boolean;
|
|
||||||
CipherSuite: number;
|
|
||||||
CurveID: number;
|
|
||||||
NegotiatedProtocol: string;
|
|
||||||
NegotiatedProtocolIsMutual: boolean;
|
|
||||||
ServerName: string;
|
|
||||||
PeerCertificates: x509.Certificate[];
|
|
||||||
VerifiedChains: x509.Certificate[][];
|
|
||||||
SignedCertificateTimestamps: number[][];
|
|
||||||
OCSPResponse: number[];
|
|
||||||
TLSUnique: number[];
|
|
||||||
ECHAccepted: boolean;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new ConnectionState(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Version = source["Version"];
|
|
||||||
this.HandshakeComplete = source["HandshakeComplete"];
|
|
||||||
this.DidResume = source["DidResume"];
|
|
||||||
this.CipherSuite = source["CipherSuite"];
|
|
||||||
this.CurveID = source["CurveID"];
|
|
||||||
this.NegotiatedProtocol = source["NegotiatedProtocol"];
|
|
||||||
this.NegotiatedProtocolIsMutual = source["NegotiatedProtocolIsMutual"];
|
|
||||||
this.ServerName = source["ServerName"];
|
|
||||||
this.PeerCertificates = this.convertValues(source["PeerCertificates"], x509.Certificate);
|
|
||||||
this.VerifiedChains = this.convertValues(source["VerifiedChains"], x509.Certificate);
|
|
||||||
this.SignedCertificateTimestamps = source["SignedCertificateTimestamps"];
|
|
||||||
this.OCSPResponse = source["OCSPResponse"];
|
|
||||||
this.TLSUnique = source["TLSUnique"];
|
|
||||||
this.ECHAccepted = source["ECHAccepted"];
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace url {
|
|
||||||
|
|
||||||
export class Userinfo {
|
|
||||||
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Userinfo(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class URL {
|
|
||||||
Scheme: string;
|
|
||||||
Opaque: string;
|
|
||||||
// Go type: Userinfo
|
|
||||||
User?: any;
|
|
||||||
Host: string;
|
|
||||||
Path: string;
|
|
||||||
RawPath: string;
|
|
||||||
OmitHost: boolean;
|
|
||||||
ForceQuery: boolean;
|
|
||||||
RawQuery: string;
|
|
||||||
Fragment: string;
|
|
||||||
RawFragment: string;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new URL(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Scheme = source["Scheme"];
|
|
||||||
this.Opaque = source["Opaque"];
|
|
||||||
this.User = this.convertValues(source["User"], null);
|
|
||||||
this.Host = source["Host"];
|
|
||||||
this.Path = source["Path"];
|
|
||||||
this.RawPath = source["RawPath"];
|
|
||||||
this.OmitHost = source["OmitHost"];
|
|
||||||
this.ForceQuery = source["ForceQuery"];
|
|
||||||
this.RawQuery = source["RawQuery"];
|
|
||||||
this.Fragment = source["Fragment"];
|
|
||||||
this.RawFragment = source["RawFragment"];
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace x509 {
|
|
||||||
|
|
||||||
export class PolicyMapping {
|
|
||||||
// Go type: OID
|
|
||||||
IssuerDomainPolicy: any;
|
|
||||||
// Go type: OID
|
|
||||||
SubjectDomainPolicy: any;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new PolicyMapping(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.IssuerDomainPolicy = this.convertValues(source["IssuerDomainPolicy"], null);
|
|
||||||
this.SubjectDomainPolicy = this.convertValues(source["SubjectDomainPolicy"], null);
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class OID {
|
|
||||||
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new OID(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class Certificate {
|
|
||||||
Raw: number[];
|
|
||||||
RawTBSCertificate: number[];
|
|
||||||
RawSubjectPublicKeyInfo: number[];
|
|
||||||
RawSubject: number[];
|
|
||||||
RawIssuer: number[];
|
|
||||||
Signature: number[];
|
|
||||||
SignatureAlgorithm: number;
|
|
||||||
PublicKeyAlgorithm: number;
|
|
||||||
PublicKey: any;
|
|
||||||
Version: number;
|
|
||||||
// Go type: big
|
|
||||||
SerialNumber?: any;
|
|
||||||
Issuer: pkix.Name;
|
|
||||||
Subject: pkix.Name;
|
|
||||||
// Go type: time
|
|
||||||
NotBefore: any;
|
|
||||||
// Go type: time
|
|
||||||
NotAfter: any;
|
|
||||||
KeyUsage: number;
|
|
||||||
Extensions: pkix.Extension[];
|
|
||||||
ExtraExtensions: pkix.Extension[];
|
|
||||||
UnhandledCriticalExtensions: number[][];
|
|
||||||
ExtKeyUsage: number[];
|
|
||||||
UnknownExtKeyUsage: number[][];
|
|
||||||
BasicConstraintsValid: boolean;
|
|
||||||
IsCA: boolean;
|
|
||||||
MaxPathLen: number;
|
|
||||||
MaxPathLenZero: boolean;
|
|
||||||
SubjectKeyId: number[];
|
|
||||||
AuthorityKeyId: number[];
|
|
||||||
OCSPServer: string[];
|
|
||||||
IssuingCertificateURL: string[];
|
|
||||||
DNSNames: string[];
|
|
||||||
EmailAddresses: string[];
|
|
||||||
IPAddresses: number[][];
|
|
||||||
URIs: url.URL[];
|
|
||||||
PermittedDNSDomainsCritical: boolean;
|
|
||||||
PermittedDNSDomains: string[];
|
|
||||||
ExcludedDNSDomains: string[];
|
|
||||||
PermittedIPRanges: net.IPNet[];
|
|
||||||
ExcludedIPRanges: net.IPNet[];
|
|
||||||
PermittedEmailAddresses: string[];
|
|
||||||
ExcludedEmailAddresses: string[];
|
|
||||||
PermittedURIDomains: string[];
|
|
||||||
ExcludedURIDomains: string[];
|
|
||||||
CRLDistributionPoints: string[];
|
|
||||||
PolicyIdentifiers: number[][];
|
|
||||||
Policies: OID[];
|
|
||||||
InhibitAnyPolicy: number;
|
|
||||||
InhibitAnyPolicyZero: boolean;
|
|
||||||
InhibitPolicyMapping: number;
|
|
||||||
InhibitPolicyMappingZero: boolean;
|
|
||||||
RequireExplicitPolicy: number;
|
|
||||||
RequireExplicitPolicyZero: boolean;
|
|
||||||
PolicyMappings: PolicyMapping[];
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new Certificate(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.Raw = source["Raw"];
|
|
||||||
this.RawTBSCertificate = source["RawTBSCertificate"];
|
|
||||||
this.RawSubjectPublicKeyInfo = source["RawSubjectPublicKeyInfo"];
|
|
||||||
this.RawSubject = source["RawSubject"];
|
|
||||||
this.RawIssuer = source["RawIssuer"];
|
|
||||||
this.Signature = source["Signature"];
|
|
||||||
this.SignatureAlgorithm = source["SignatureAlgorithm"];
|
|
||||||
this.PublicKeyAlgorithm = source["PublicKeyAlgorithm"];
|
|
||||||
this.PublicKey = source["PublicKey"];
|
|
||||||
this.Version = source["Version"];
|
|
||||||
this.SerialNumber = this.convertValues(source["SerialNumber"], null);
|
|
||||||
this.Issuer = this.convertValues(source["Issuer"], pkix.Name);
|
|
||||||
this.Subject = this.convertValues(source["Subject"], pkix.Name);
|
|
||||||
this.NotBefore = this.convertValues(source["NotBefore"], null);
|
|
||||||
this.NotAfter = this.convertValues(source["NotAfter"], null);
|
|
||||||
this.KeyUsage = source["KeyUsage"];
|
|
||||||
this.Extensions = this.convertValues(source["Extensions"], pkix.Extension);
|
|
||||||
this.ExtraExtensions = this.convertValues(source["ExtraExtensions"], pkix.Extension);
|
|
||||||
this.UnhandledCriticalExtensions = source["UnhandledCriticalExtensions"];
|
|
||||||
this.ExtKeyUsage = source["ExtKeyUsage"];
|
|
||||||
this.UnknownExtKeyUsage = source["UnknownExtKeyUsage"];
|
|
||||||
this.BasicConstraintsValid = source["BasicConstraintsValid"];
|
|
||||||
this.IsCA = source["IsCA"];
|
|
||||||
this.MaxPathLen = source["MaxPathLen"];
|
|
||||||
this.MaxPathLenZero = source["MaxPathLenZero"];
|
|
||||||
this.SubjectKeyId = source["SubjectKeyId"];
|
|
||||||
this.AuthorityKeyId = source["AuthorityKeyId"];
|
|
||||||
this.OCSPServer = source["OCSPServer"];
|
|
||||||
this.IssuingCertificateURL = source["IssuingCertificateURL"];
|
|
||||||
this.DNSNames = source["DNSNames"];
|
|
||||||
this.EmailAddresses = source["EmailAddresses"];
|
|
||||||
this.IPAddresses = source["IPAddresses"];
|
|
||||||
this.URIs = this.convertValues(source["URIs"], url.URL);
|
|
||||||
this.PermittedDNSDomainsCritical = source["PermittedDNSDomainsCritical"];
|
|
||||||
this.PermittedDNSDomains = source["PermittedDNSDomains"];
|
|
||||||
this.ExcludedDNSDomains = source["ExcludedDNSDomains"];
|
|
||||||
this.PermittedIPRanges = this.convertValues(source["PermittedIPRanges"], net.IPNet);
|
|
||||||
this.ExcludedIPRanges = this.convertValues(source["ExcludedIPRanges"], net.IPNet);
|
|
||||||
this.PermittedEmailAddresses = source["PermittedEmailAddresses"];
|
|
||||||
this.ExcludedEmailAddresses = source["ExcludedEmailAddresses"];
|
|
||||||
this.PermittedURIDomains = source["PermittedURIDomains"];
|
|
||||||
this.ExcludedURIDomains = source["ExcludedURIDomains"];
|
|
||||||
this.CRLDistributionPoints = source["CRLDistributionPoints"];
|
|
||||||
this.PolicyIdentifiers = source["PolicyIdentifiers"];
|
|
||||||
this.Policies = this.convertValues(source["Policies"], OID);
|
|
||||||
this.InhibitAnyPolicy = source["InhibitAnyPolicy"];
|
|
||||||
this.InhibitAnyPolicyZero = source["InhibitAnyPolicyZero"];
|
|
||||||
this.InhibitPolicyMapping = source["InhibitPolicyMapping"];
|
|
||||||
this.InhibitPolicyMappingZero = source["InhibitPolicyMappingZero"];
|
|
||||||
this.RequireExplicitPolicy = source["RequireExplicitPolicy"];
|
|
||||||
this.RequireExplicitPolicyZero = source["RequireExplicitPolicyZero"];
|
|
||||||
this.PolicyMappings = this.convertValues(source["PolicyMappings"], PolicyMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
|
||||||
if (!a) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
if (a.slice && a.map) {
|
|
||||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
|
||||||
} else if ("object" === typeof a) {
|
|
||||||
if (asMap) {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
a[key] = new classs(a[key]);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return new classs(a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ require (
|
|||||||
github.com/a-h/templ v0.3.977
|
github.com/a-h/templ v0.3.977
|
||||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||||
github.com/golang-cz/devslog v0.0.15
|
github.com/golang-cz/devslog v0.0.15
|
||||||
github.com/gorilla/schema v1.4.1
|
|
||||||
github.com/wailsapp/wails/v2 v2.10.2
|
github.com/wailsapp/wails/v2 v2.10.2
|
||||||
golang.org/x/image v0.12.0
|
golang.org/x/image v0.12.0
|
||||||
golang.org/x/sync v0.19.0
|
golang.org/x/sync v0.19.0
|
||||||
|
|||||||
@@ -461,8 +461,6 @@ github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk
|
|||||||
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
|
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
|
||||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
|
||||||
"htmx": "^0.0.2"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user