added color/ theme settings, new config window
This commit is contained in:
@@ -65,7 +65,6 @@ func NewYellowJacketApp(
|
||||
}
|
||||
|
||||
yjApp.appConfig = appConfig
|
||||
yjApp.assetHandler.RegisterHandler("/config", yjApp.appConfig)
|
||||
|
||||
// create frontendUtil
|
||||
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"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
@@ -16,17 +15,17 @@ import (
|
||||
"yellowjacket/backend/events"
|
||||
"yellowjacket/backend/library"
|
||||
"yellowjacket/backend/system"
|
||||
"yellowjacket/backend/theme"
|
||||
)
|
||||
|
||||
// Config represents the application configuration.
|
||||
type Config struct {
|
||||
ctx context.Context
|
||||
logger *slog.Logger
|
||||
serveMux *http.ServeMux
|
||||
filePath string // required
|
||||
Library *library.Config `form:"Library" schema:"library,required"`
|
||||
|
||||
Window *WindowConfig `toml:"Window"`
|
||||
Library *library.Config `toml:"Library"`
|
||||
Theme *theme.Config `toml:"Theme"`
|
||||
Window *WindowConfig `toml:"Window"`
|
||||
}
|
||||
|
||||
// NewConfig creates a new config by loading it from disk.
|
||||
@@ -38,11 +37,9 @@ func NewConfig(logger *slog.Logger) (*Config, error) {
|
||||
|
||||
conf := &Config{
|
||||
filePath: path.Join(confDir, "config.toml"),
|
||||
serveMux: http.NewServeMux(),
|
||||
}
|
||||
conf.applyDefaults()
|
||||
conf.logger = logger.WithGroup("config").With("config", conf)
|
||||
conf.serveMux.HandleFunc("/", conf.handle)
|
||||
|
||||
if err := conf.Load(); err != nil {
|
||||
return nil, fmt.Errorf("could not load config: %w", err)
|
||||
@@ -67,8 +64,17 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Theme != nil {
|
||||
if err := c.Theme.Validate(); err != nil {
|
||||
configErrs = errors.Join(configErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if configErrs != nil {
|
||||
return fmt.Errorf("one or more config parts are invalid: %w", configErrs)
|
||||
return fmt.Errorf(
|
||||
"one or more config parts are invalid: %w",
|
||||
configErrs,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -148,6 +154,12 @@ func (c *Config) applyDefaults() {
|
||||
if c.Library != nil {
|
||||
c.Library.ApplyDefaults()
|
||||
}
|
||||
|
||||
if c.Theme == nil {
|
||||
c.Theme = &theme.Config{}
|
||||
}
|
||||
|
||||
c.Theme.ApplyDefaults()
|
||||
}
|
||||
|
||||
// SetContext sets the Wails runtime context for event emission.
|
||||
@@ -245,3 +257,103 @@ func (c *Config) SetScanConcurrency(mode string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetThemeAccentColor returns the configured accent colour.
|
||||
func (c *Config) GetThemeAccentColor() string {
|
||||
if c.Theme == nil {
|
||||
return theme.DefaultAccentColor
|
||||
}
|
||||
|
||||
return c.Theme.AccentColor
|
||||
}
|
||||
|
||||
// GetThemeBackgroundShade returns the configured background shade.
|
||||
func (c *Config) GetThemeBackgroundShade() string {
|
||||
if c.Theme == nil {
|
||||
return string(theme.DefaultBackgroundShade)
|
||||
}
|
||||
|
||||
return string(c.Theme.BackgroundShade)
|
||||
}
|
||||
|
||||
// SetThemeAccentColor validates and saves a new accent colour.
|
||||
func (c *Config) SetThemeAccentColor(
|
||||
color string,
|
||||
) error {
|
||||
if c.Theme == nil {
|
||||
c.Theme = &theme.Config{}
|
||||
c.Theme.ApplyDefaults()
|
||||
}
|
||||
|
||||
c.Theme.AccentColor = color
|
||||
|
||||
if err := c.Theme.Validate(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"invalid theme accent color: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not save config: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
c.emitThemeChanged()
|
||||
|
||||
c.logger.Info(
|
||||
"theme accent color updated",
|
||||
"color", color,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetThemeBackgroundShade validates and saves a new background shade.
|
||||
func (c *Config) SetThemeBackgroundShade(
|
||||
shade string,
|
||||
) error {
|
||||
if c.Theme == nil {
|
||||
c.Theme = &theme.Config{}
|
||||
c.Theme.ApplyDefaults()
|
||||
}
|
||||
|
||||
c.Theme.BackgroundShade = theme.BackgroundShade(shade)
|
||||
|
||||
if err := c.Theme.Validate(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"invalid theme background shade: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not save config: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
c.emitThemeChanged()
|
||||
|
||||
c.logger.Info(
|
||||
"theme background shade updated",
|
||||
"shade", shade,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// emitThemeChanged sends the ThemeConfigChanged event to the frontend.
|
||||
func (c *Config) emitThemeChanged() {
|
||||
if c.ctx == nil || c.Theme == nil {
|
||||
return
|
||||
}
|
||||
|
||||
runtime.EventsEmit(
|
||||
c.ctx,
|
||||
events.ThemeConfigChanged,
|
||||
map[string]any{
|
||||
"AccentColor": c.Theme.AccentColor,
|
||||
"BackgroundShade": string(c.Theme.BackgroundShade),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
const (
|
||||
LibraryConfigChanged = "LibraryConfigChanged"
|
||||
ThemeConfigChanged = "ThemeConfigChanged"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
background-color: black;
|
||||
color: white;
|
||||
background-color: var(--yj-bg-base, black);
|
||||
color: var(--yj-text-primary, white);
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
@@ -32,7 +32,7 @@ p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #343a40;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -51,14 +51,14 @@ ul {
|
||||
|
||||
body div.sidebar {
|
||||
grid-area: sidebar;
|
||||
background-color: #212529;
|
||||
background-color: var(--yj-bg-surface, #212529);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
grid-area: bottom-bar;
|
||||
padding: 0.25em;
|
||||
background-color: #343a40;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
display: grid;
|
||||
grid-template-columns: var(--now-playing-width, 200px) 1fr auto;
|
||||
align-items: center;
|
||||
@@ -75,7 +75,7 @@ body div.sidebar {
|
||||
height: 3.5em;
|
||||
min-height: 3.5em;
|
||||
border-radius: 0.25em;
|
||||
background-color: #ffd43b;
|
||||
background-color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
#track-info {
|
||||
@@ -114,12 +114,12 @@ body div.sidebar {
|
||||
}
|
||||
|
||||
#queue-button:hover {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
#queue-button.drag-over {
|
||||
color: #ffd43b;
|
||||
outline: 2px dashed #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
outline: 2px dashed var(--yj-accent, #ffd43b);
|
||||
outline-offset: -2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ body div.sidebar {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.25em;
|
||||
background-color: #212529;
|
||||
background-color: var(--yj-bg-surface, #212529);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
<h1 class="title">YellowJacket</h1>
|
||||
<h3 class="subtitle">Music how it was meant to bee.</h3>
|
||||
</hgroup>
|
||||
<a href="/src/pages/config/">
|
||||
<img src="/src/assets/images/icons/ui/settings.svg" />
|
||||
</a>
|
||||
</header>
|
||||
<div class="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/playlist-view/playlist-view.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/components/icon/icon.js';
|
||||
import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { playlistStore } from '@store/playlist-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 {
|
||||
hasTrackPayload,
|
||||
getDragPayload,
|
||||
@@ -46,8 +50,11 @@ document.addEventListener('navigate', (e: Event) => {
|
||||
case 'libraries':
|
||||
mainContent.innerHTML = '<library-manager></library-manager>';
|
||||
break;
|
||||
case 'settings':
|
||||
mainContent.innerHTML = '<config-page></config-page>';
|
||||
break;
|
||||
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>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"@awesome.me/webawesome": "^3.2.1",
|
||||
"@lit-labs/signals": "^0.2.0",
|
||||
"@lit-labs/virtualizer": "^2.1.1",
|
||||
"htmx.org": "2.0.8",
|
||||
"lit": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -58,11 +58,11 @@ export class PlayerControls extends LitElement {
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.repeat-one {
|
||||
|
||||
@@ -22,23 +22,23 @@ export class SeekBar extends LitElement {
|
||||
--track-size: 6px;
|
||||
flex: 1;
|
||||
margin: 0 1em;
|
||||
--wa-tooltip-background-color: #343a40;
|
||||
--wa-tooltip-content-color: white;
|
||||
--wa-tooltip-border-color: #343a40;
|
||||
--wa-tooltip-background-color: var(--yj-bg-elevated, #343a40);
|
||||
--wa-tooltip-content-color: var(--yj-text-primary, white);
|
||||
--wa-tooltip-border-color: var(--yj-bg-elevated, #343a40);
|
||||
--wa-tooltip-border-radius: 4px;
|
||||
--wa-tooltip-font-size: 0.875em;
|
||||
}
|
||||
|
||||
wa-slider::part(track) {
|
||||
background: white;
|
||||
background: var(--yj-text-primary, white);
|
||||
}
|
||||
|
||||
wa-slider::part(indicator) {
|
||||
background: yellow;
|
||||
background: var(--yj-accent, yellow);
|
||||
}
|
||||
|
||||
wa-slider::part(thumb) {
|
||||
background: black;
|
||||
background: var(--yj-bg-base, black);
|
||||
}
|
||||
|
||||
#seek-bar-container {
|
||||
|
||||
@@ -35,8 +35,8 @@ export class VolumeControl extends LitElement {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background: var(--yj-bg-surface, #1a1a1a);
|
||||
border: 1px solid var(--yj-border-subtle, #333);
|
||||
border-radius: 8px;
|
||||
padding: 1em 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
@@ -52,16 +52,16 @@ export class VolumeControl extends LitElement {
|
||||
}
|
||||
|
||||
wa-slider::part(track) {
|
||||
background: white;
|
||||
background: var(--yj-text-primary, white);
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
wa-slider::part(indicator) {
|
||||
background: yellow;
|
||||
background: var(--yj-accent, yellow);
|
||||
}
|
||||
|
||||
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 {
|
||||
background-color: #343a40;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
@@ -81,7 +81,7 @@ export class AlbumDropdown extends LitElement {
|
||||
height: 0;
|
||||
border-left: 9px solid transparent;
|
||||
border-right: 9px solid transparent;
|
||||
border-bottom: 8px solid #343a40;
|
||||
border-bottom: 8px solid var(--yj-bg-elevated, #343a40);
|
||||
}
|
||||
|
||||
.dropdown-tracks {
|
||||
@@ -103,11 +103,9 @@ export class AlbumDropdown extends LitElement {
|
||||
}
|
||||
|
||||
.track-row:hover {
|
||||
background-color: rgba(
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
0.05
|
||||
background-color: var(
|
||||
--yj-hover-overlay,
|
||||
rgba(255, 255, 255, 0.05)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,13 +119,11 @@ export class AlbumDropdown extends LitElement {
|
||||
}
|
||||
|
||||
.track-row.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-row.selected.active {
|
||||
@@ -140,14 +136,14 @@ export class AlbumDropdown extends LitElement {
|
||||
}
|
||||
|
||||
.track-number {
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
min-width: 22px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.track-title {
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -156,7 +152,7 @@ export class AlbumDropdown extends LitElement {
|
||||
}
|
||||
|
||||
.track-duration {
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -203,21 +203,21 @@ export class CoverGrid extends LitElement {
|
||||
}
|
||||
|
||||
.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 {
|
||||
outline: 2px solid #ffd43b;
|
||||
outline: 2px solid var(--yj-accent, #ffd43b);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.album-card.expanded {
|
||||
background-color: #343a40;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.album-card:focus-visible {
|
||||
outline: 2px solid #ffd43b;
|
||||
outline: 2px solid var(--yj-accent, #ffd43b);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ export class CoverGrid extends LitElement {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #282828;
|
||||
background-color: var(--yj-bg-surface, #282828);
|
||||
transition: scale 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -263,8 +263,8 @@ export class CoverGrid extends LitElement {
|
||||
|
||||
.select-toggle.checked {
|
||||
opacity: 1;
|
||||
background-color: #ffd43b;
|
||||
border-color: #ffd43b;
|
||||
background-color: var(--yj-accent, #ffd43b);
|
||||
border-color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
@@ -289,10 +289,10 @@ export class CoverGrid extends LitElement {
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#404040 0%,
|
||||
#282828 100%
|
||||
var(--yj-bg-overlay, #404040) 0%,
|
||||
var(--yj-bg-surface, #282828) 100%
|
||||
);
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
font-size: var(--placeholder-font, 48px);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ export class CoverGrid extends LitElement {
|
||||
.album-name {
|
||||
font-size: var(--album-name-font, 14px);
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -318,7 +318,7 @@ export class CoverGrid extends LitElement {
|
||||
|
||||
.artist-name {
|
||||
font-size: var(--artist-name-font, 12px);
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -326,7 +326,7 @@ export class CoverGrid extends LitElement {
|
||||
}
|
||||
|
||||
.album-year {
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
@@ -338,7 +338,7 @@ export class CoverGrid extends LitElement {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 32px;
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -347,7 +347,7 @@ export class CoverGrid extends LitElement {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 48px;
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -364,8 +364,8 @@ export class CoverGrid extends LitElement {
|
||||
}
|
||||
|
||||
.context-menu-panel {
|
||||
background-color: #343a40;
|
||||
border: 1px solid #444;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
border: 1px solid var(--yj-border, #444);
|
||||
border-radius: 6px;
|
||||
padding: 4px 0;
|
||||
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 {
|
||||
--wa-color-text-normal: #fff;
|
||||
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ export class LibraryManager extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
padding: 1.5em;
|
||||
color: #e9ecef;
|
||||
color: var(--yj-text-primary, #e9ecef);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -251,13 +251,13 @@ export class LibraryManager extends LitElement {
|
||||
margin: 0 0 1em 0;
|
||||
font-size: 1.4em;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
color: var(--yj-text-primary, #f8f9fa);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 2em;
|
||||
padding: 1.25em;
|
||||
background: #2b3035;
|
||||
background: var(--yj-bg-surface, #2b3035);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ export class LibraryManager extends LitElement {
|
||||
margin: 0 0 0.75em 0;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
color: #dee2e6;
|
||||
color: var(--yj-text-primary, #dee2e6);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@@ -282,7 +282,7 @@ export class LibraryManager extends LitElement {
|
||||
.section-description {
|
||||
margin: 0 0 1em 0;
|
||||
font-size: 0.85em;
|
||||
color: #868e96;
|
||||
color: var(--yj-text-tertiary, #868e96);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -296,10 +296,10 @@ export class LibraryManager extends LitElement {
|
||||
.directory-path {
|
||||
flex: 1;
|
||||
padding: 0.5em 0.75em;
|
||||
background: #1a1d20;
|
||||
border: 1px solid #495057;
|
||||
background: var(--yj-bg-elevated, #1a1d20);
|
||||
border: 1px solid var(--yj-bg-overlay, #495057);
|
||||
border-radius: 4px;
|
||||
color: #adb5bd;
|
||||
color: var(--yj-text-secondary, #adb5bd);
|
||||
font-size: 0.85em;
|
||||
font-family: monospace;
|
||||
overflow: hidden;
|
||||
@@ -309,7 +309,7 @@ export class LibraryManager extends LitElement {
|
||||
}
|
||||
|
||||
.directory-path.has-value {
|
||||
color: #e9ecef;
|
||||
color: var(--yj-text-primary, #e9ecef);
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -329,57 +329,57 @@ export class LibraryManager extends LitElement {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4263eb;
|
||||
background: var(--yj-info, #4263eb);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #3b5bdb;
|
||||
background: var(--yj-info-hover, #3b5bdb);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #2f9e44;
|
||||
background: var(--yj-success, #2f9e44);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #2b8a3e;
|
||||
background: var(--yj-success-hover, #2b8a3e);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #e8590c;
|
||||
background: var(--yj-warning, #e8590c);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #d9480f;
|
||||
background: var(--yj-warning-hover, #d9480f);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e03131;
|
||||
background: var(--yj-error, #e03131);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #c92a2a;
|
||||
background: var(--yj-error-hover, #c92a2a);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: #868e96;
|
||||
color: var(--yj-text-tertiary, #868e96);
|
||||
padding: 0.3em 0.75em;
|
||||
font-size: 0.75em;
|
||||
border: 1px solid #495057;
|
||||
border: 1px solid var(--yj-bg-overlay, #495057);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background: #495057;
|
||||
color: #e9ecef;
|
||||
background: var(--yj-bg-overlay, #495057);
|
||||
color: var(--yj-text-primary, #e9ecef);
|
||||
}
|
||||
|
||||
.btn-ghost.copied {
|
||||
border-color: #2f9e44;
|
||||
color: #2f9e44;
|
||||
border-color: var(--yj-success, #2f9e44);
|
||||
color: var(--yj-success, #2f9e44);
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
@@ -390,16 +390,16 @@ export class LibraryManager extends LitElement {
|
||||
}
|
||||
|
||||
.setting-row label {
|
||||
color: #adb5bd;
|
||||
color: var(--yj-text-secondary, #adb5bd);
|
||||
min-width: 8em;
|
||||
}
|
||||
|
||||
.setting-row select {
|
||||
padding: 0.4em 0.6em;
|
||||
background: #1a1d20;
|
||||
border: 1px solid #495057;
|
||||
background: var(--yj-bg-elevated, #1a1d20);
|
||||
border: 1px solid var(--yj-bg-overlay, #495057);
|
||||
border-radius: 4px;
|
||||
color: #e9ecef;
|
||||
color: var(--yj-text-primary, #e9ecef);
|
||||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
@@ -408,12 +408,12 @@ export class LibraryManager extends LitElement {
|
||||
|
||||
.setting-row select:focus {
|
||||
outline: none;
|
||||
border-color: #4263eb;
|
||||
border-color: var(--yj-info, #4263eb);
|
||||
}
|
||||
|
||||
.setting-row select option {
|
||||
background: #2b3035;
|
||||
color: #e9ecef;
|
||||
background: var(--yj-bg-surface, #2b3035);
|
||||
color: var(--yj-text-primary, #e9ecef);
|
||||
}
|
||||
|
||||
.scan-actions {
|
||||
@@ -425,15 +425,15 @@ export class LibraryManager extends LitElement {
|
||||
.status-bar {
|
||||
margin-top: 1.5em;
|
||||
padding: 0.75em 1em;
|
||||
background: #1a1d20;
|
||||
background: var(--yj-bg-elevated, #1a1d20);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
color: #868e96;
|
||||
color: var(--yj-text-tertiary, #868e96);
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.status-bar.active {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
/* --- Metrics tree --- */
|
||||
@@ -453,7 +453,7 @@ export class LibraryManager extends LitElement {
|
||||
cursor: pointer;
|
||||
padding: 0.25em 0;
|
||||
font-size: 0.85em;
|
||||
color: #ced4da;
|
||||
color: var(--yj-text-secondary, #ced4da);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -484,21 +484,21 @@ export class LibraryManager extends LitElement {
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #adb5bd;
|
||||
color: var(--yj-text-secondary, #adb5bd);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #e9ecef;
|
||||
color: var(--yj-text-primary, #e9ecef);
|
||||
font-family: monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-value.highlight {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.metric-note {
|
||||
color: #868e96;
|
||||
color: var(--yj-text-tertiary, #868e96);
|
||||
font-size: 0.75em;
|
||||
font-style: italic;
|
||||
padding-left: 1.35em;
|
||||
@@ -513,11 +513,11 @@ export class LibraryManager extends LitElement {
|
||||
}
|
||||
|
||||
.count-label {
|
||||
color: #adb5bd;
|
||||
color: var(--yj-text-secondary, #adb5bd);
|
||||
}
|
||||
|
||||
.count-value {
|
||||
color: #e9ecef;
|
||||
color: var(--yj-text-primary, #e9ecef);
|
||||
font-family: monospace;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -48,14 +48,14 @@ export class NowPlaying extends LitElement {
|
||||
.cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
background-color: var(--yj-bg-base, #000);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cover-placeholder wa-icon {
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class NowPlaying extends LitElement {
|
||||
|
||||
.track-artist {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--yj-text-tertiary, #666);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -96,7 +96,7 @@ export class NowPlaying extends LitElement {
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle.dragging {
|
||||
background-color: #6c757d;
|
||||
background-color: var(--yj-text-tertiary, #6c757d);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ export class PlaylistPicker extends LitElement {
|
||||
}
|
||||
|
||||
.picker-panel {
|
||||
background-color: #343a40;
|
||||
border: 1px solid #444;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
border: 1px solid var(--yj-border, #444);
|
||||
border-radius: 6px;
|
||||
padding: 4px 0;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
--wa-color-text-normal: #fff;
|
||||
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
height: 1px;
|
||||
background: #555;
|
||||
background: var(--yj-border-subtle, #555);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ export class PlaylistPicker extends LitElement {
|
||||
}
|
||||
|
||||
.create-form input {
|
||||
background: #2a2d30;
|
||||
border: 1px solid #555;
|
||||
background: var(--yj-bg-surface, #2b3035);
|
||||
border: 1px solid var(--yj-border-subtle, #555);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
@@ -82,11 +82,11 @@ export class PlaylistPicker extends LitElement {
|
||||
}
|
||||
|
||||
.create-form input:focus {
|
||||
border-color: #ffd43b;
|
||||
border-color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.create-form input::placeholder {
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
@@ -96,10 +96,10 @@ export class PlaylistPicker extends LitElement {
|
||||
}
|
||||
|
||||
.button-row button {
|
||||
background: #495057;
|
||||
background: var(--yj-bg-overlay, #495057);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
@@ -111,23 +111,23 @@ export class PlaylistPicker extends LitElement {
|
||||
}
|
||||
|
||||
.button-row button.primary {
|
||||
background: #ffd43b;
|
||||
background: var(--yj-accent, #ffd43b);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.button-row button.primary:hover {
|
||||
background: #ffe066;
|
||||
background: var(--yj-accent-hover, #ffe066);
|
||||
}
|
||||
|
||||
.button-row button.primary:disabled {
|
||||
background: #665a1e;
|
||||
color: #888;
|
||||
background: var(--yj-accent-muted, #665a1e);
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
padding: 8px 12px;
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
font-size: 13px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -205,21 +205,21 @@ export class PlaylistView
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #333;
|
||||
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
}
|
||||
|
||||
.new-playlist-button {
|
||||
background: none;
|
||||
border: 1px solid #555;
|
||||
border: 1px solid var(--yj-border-subtle, #555);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
@@ -230,8 +230,8 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.new-playlist-button:hover {
|
||||
border-color: #ffd43b;
|
||||
color: #ffd43b;
|
||||
border-color: var(--yj-accent, #ffd43b);
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.create-form {
|
||||
@@ -239,16 +239,16 @@ export class PlaylistView
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #333;
|
||||
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.create-form input {
|
||||
flex: 1;
|
||||
background: #2a2d30;
|
||||
border: 1px solid #555;
|
||||
background: var(--yj-bg-surface, #2b3035);
|
||||
border: 1px solid var(--yj-border-subtle, #555);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
@@ -256,18 +256,18 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.create-form input:focus {
|
||||
border-color: #ffd43b;
|
||||
border-color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.create-form input::placeholder {
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
}
|
||||
|
||||
.create-form button {
|
||||
background: #495057;
|
||||
background: var(--yj-bg-overlay, #495057);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
@@ -275,21 +275,21 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.create-form button:hover {
|
||||
background: #5a6268;
|
||||
background: var(--yj-bg-overlay, #5a6268);
|
||||
}
|
||||
|
||||
.create-form button.primary {
|
||||
background: #ffd43b;
|
||||
background: var(--yj-accent, #ffd43b);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.create-form button.primary:hover {
|
||||
background: #ffe066;
|
||||
background: var(--yj-accent-hover, #ffe066);
|
||||
}
|
||||
|
||||
.create-form button.primary:disabled {
|
||||
background: #665a1e;
|
||||
color: #888;
|
||||
background: var(--yj-accent-muted, #665a1e);
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ export class PlaylistView
|
||||
|
||||
.playlist-item {
|
||||
border-bottom: 1px solid
|
||||
rgba(255, 255, 255, 0.05);
|
||||
var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
@@ -316,18 +316,18 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.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 {
|
||||
background-color: rgba(255, 212, 59, 0.15);
|
||||
outline: 1px dashed #ffd43b;
|
||||
background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
|
||||
outline: 1px dashed var(--yj-accent, #ffd43b);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
@@ -338,13 +338,13 @@ export class PlaylistView
|
||||
|
||||
.playlist-icon {
|
||||
font-size: 18px;
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.playlist-name {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -353,7 +353,7 @@ export class PlaylistView
|
||||
|
||||
.track-count {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
color: var(--yj-text-tertiary, #666);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -370,9 +370,9 @@ export class PlaylistView
|
||||
|
||||
.play-all-button {
|
||||
background: none;
|
||||
border: 1px solid #555;
|
||||
border: 1px solid var(--yj-border-subtle, #555);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
@@ -383,8 +383,8 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.play-all-button:hover {
|
||||
border-color: #ffd43b;
|
||||
color: #ffd43b;
|
||||
border-color: var(--yj-accent, #ffd43b);
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.track-item {
|
||||
@@ -396,20 +396,20 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.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 {
|
||||
background-color: rgba(100, 160, 255, 0.15);
|
||||
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||
}
|
||||
|
||||
.track-item.active {
|
||||
background-color: rgba(255, 212, 59, 0.1);
|
||||
color: #ffd43b;
|
||||
background-color: var(--yj-accent-bg, rgba(255, 212, 59, 0.1));
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -424,7 +424,7 @@ export class PlaylistView
|
||||
.phantom-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
color: #e67700;
|
||||
color: var(--yj-warning, #e67700);
|
||||
background: rgba(230, 119, 0, 0.15);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
@@ -438,7 +438,7 @@ export class PlaylistView
|
||||
|
||||
.tracks-empty {
|
||||
padding: 12px 0;
|
||||
color: #666;
|
||||
color: var(--yj-text-tertiary, #666);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ export class PlaylistView
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 32px;
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -456,7 +456,7 @@ export class PlaylistView
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 20px;
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -474,8 +474,8 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.context-menu-panel {
|
||||
background-color: #343a40;
|
||||
border: 1px solid #444;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
border: 1px solid var(--yj-border, #444);
|
||||
border-radius: 6px;
|
||||
padding: 4px 0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
@@ -484,7 +484,7 @@ export class PlaylistView
|
||||
|
||||
.context-menu-panel wa-dropdown-item {
|
||||
cursor: pointer;
|
||||
--wa-color-text-normal: #fff;
|
||||
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -512,10 +512,10 @@ export class PlaylistView
|
||||
|
||||
.rename-input {
|
||||
flex: 1;
|
||||
background: #2a2d30;
|
||||
border: 1px solid #ffd43b;
|
||||
background: var(--yj-bg-surface, #2b3035);
|
||||
border: 1px solid var(--yj-accent, #ffd43b);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
@@ -525,9 +525,9 @@ export class PlaylistView
|
||||
|
||||
.import-button {
|
||||
background: none;
|
||||
border: 1px solid #555;
|
||||
border: 1px solid var(--yj-border-subtle, #555);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
@@ -538,8 +538,8 @@ export class PlaylistView
|
||||
}
|
||||
|
||||
.import-button:hover {
|
||||
border-color: #ffd43b;
|
||||
color: #ffd43b;
|
||||
border-color: var(--yj-accent, #ffd43b);
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ export class QueuePanel
|
||||
flex-shrink: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
background-color: #212529;
|
||||
background-color: var(--yj-bg-surface, #212529);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export class QueuePanel
|
||||
--queue-width,
|
||||
${unsafeCSS(DEFAULT_WIDTH)}px
|
||||
);
|
||||
border-left: 1px solid #333;
|
||||
border-left: 1px solid var(--yj-border-subtle, #333);
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
@@ -180,7 +180,7 @@ export class QueuePanel
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle.dragging {
|
||||
background-color: #6c757d;
|
||||
background-color: var(--yj-text-tertiary, #6c757d);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
@@ -196,7 +196,7 @@ export class QueuePanel
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #333;
|
||||
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -223,11 +223,11 @@ export class QueuePanel
|
||||
}
|
||||
|
||||
.header-action-button:hover {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.header-action-button:disabled {
|
||||
color: #555;
|
||||
color: var(--yj-border-subtle, #555);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ export class QueuePanel
|
||||
padding: 8px 16px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid
|
||||
rgba(255, 255, 255, 0.05);
|
||||
var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
@@ -255,30 +255,30 @@ export class QueuePanel
|
||||
}
|
||||
|
||||
.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 {
|
||||
background-color: rgba(100, 160, 255, 0.15);
|
||||
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||
}
|
||||
|
||||
.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 {
|
||||
background-color: rgba(100, 160, 255, 0.15);
|
||||
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||
}
|
||||
|
||||
.track-position {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.track-item.active .track-position {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.track-details {
|
||||
@@ -297,12 +297,12 @@ export class QueuePanel
|
||||
}
|
||||
|
||||
.track-item.active .track-title {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.track-artist {
|
||||
font-size: 11px;
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -311,7 +311,7 @@ export class QueuePanel
|
||||
.remove-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
@@ -325,11 +325,11 @@ export class QueuePanel
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
color: #ff6b6b;
|
||||
color: var(--yj-error, #ff6b6b);
|
||||
}
|
||||
|
||||
.panel-content.drag-over {
|
||||
outline: 2px dashed #ffd43b;
|
||||
outline: 2px dashed var(--yj-accent, #ffd43b);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ export class QueuePanel
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
border-bottom: 1px solid
|
||||
rgba(255, 212, 59, 0.2);
|
||||
}
|
||||
@@ -355,7 +355,7 @@ export class QueuePanel
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 2px;
|
||||
background: #ffd43b;
|
||||
background: var(--yj-accent, #ffd43b);
|
||||
border-radius: 1px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
@@ -368,7 +368,7 @@ export class QueuePanel
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 2px;
|
||||
background: #ffd43b;
|
||||
background: var(--yj-accent, #ffd43b);
|
||||
border-radius: 1px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
@@ -380,7 +380,7 @@ export class QueuePanel
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: #b3b3b3;
|
||||
color: var(--yj-text-secondary, #b3b3b3);
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -394,8 +394,8 @@ export class QueuePanel
|
||||
}
|
||||
|
||||
.context-menu-panel {
|
||||
background-color: #343a40;
|
||||
border: 1px solid #444;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
border: 1px solid var(--yj-border, #444);
|
||||
border-radius: 6px;
|
||||
padding: 4px 0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
@@ -404,7 +404,7 @@ export class QueuePanel
|
||||
|
||||
.context-menu-panel wa-dropdown-item {
|
||||
cursor: pointer;
|
||||
--wa-color-text-normal: #fff;
|
||||
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
|
||||
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 {
|
||||
id: View;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 120;
|
||||
@@ -21,7 +23,7 @@ export class AppSidebar extends LitElement {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: #212529;
|
||||
background-color: var(--yj-bg-surface, #212529);
|
||||
min-width: ${MIN_WIDTH}px;
|
||||
max-width: ${MAX_WIDTH}px;
|
||||
}
|
||||
@@ -40,7 +42,7 @@ export class AppSidebar extends LitElement {
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle.dragging {
|
||||
background-color: #6c757d;
|
||||
background-color: var(--yj-text-tertiary, #6c757d);
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -50,19 +52,28 @@ export class AppSidebar extends LitElement {
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
li wa-icon {
|
||||
font-size: 0.9em;
|
||||
flex-shrink: 0;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: #343a40;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
}
|
||||
|
||||
li.active {
|
||||
background-color: #495057;
|
||||
background-color: var(--yj-bg-overlay, #495057);
|
||||
}
|
||||
|
||||
li p {
|
||||
@@ -73,8 +84,8 @@ export class AppSidebar extends LitElement {
|
||||
}
|
||||
|
||||
li.drag-hover {
|
||||
background-color: rgba(255, 212, 59, 0.15);
|
||||
outline: 1px dashed #ffd43b;
|
||||
background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
|
||||
outline: 1px dashed var(--yj-accent, #ffd43b);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
`;
|
||||
@@ -101,12 +112,13 @@ export class AppSidebar extends LitElement {
|
||||
> | null = null;
|
||||
|
||||
private navItems: NavItem[] = [
|
||||
{ id: 'home', label: 'Home' },
|
||||
{ id: 'libraries', label: 'Libraries' },
|
||||
{ id: 'playlists', label: 'Playlists' },
|
||||
{ id: 'artists', label: 'Artists' },
|
||||
{ id: 'albums', label: 'Albums' },
|
||||
{ id: 'tracks', label: 'Tracks' },
|
||||
{ id: 'home', label: 'Home', icon: 'house' },
|
||||
{ id: 'libraries', label: 'Libraries', icon: 'folder-open' },
|
||||
{ id: 'playlists', label: 'Playlists', icon: 'list' },
|
||||
{ id: 'artists', label: 'Artists', icon: 'user-group' },
|
||||
{ id: 'albums', label: 'Albums', icon: 'compact-disc' },
|
||||
{ id: 'tracks', label: 'Tracks', icon: 'music' },
|
||||
{ id: 'settings', label: 'Settings', icon: 'gear' },
|
||||
];
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -179,6 +191,9 @@ export class AppSidebar extends LitElement {
|
||||
@drop=${(e: DragEvent) =>
|
||||
this.onNavDrop(e)}
|
||||
>
|
||||
<wa-icon
|
||||
name=${item.icon}
|
||||
></wa-icon>
|
||||
<p>${item.label}</p>
|
||||
</li>
|
||||
`;
|
||||
|
||||
@@ -63,14 +63,14 @@ export class TrackInfo extends LitElement {
|
||||
.cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #2a2d30;
|
||||
background-color: var(--yj-bg-elevated, #2a2d30);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cover-placeholder wa-icon {
|
||||
color: #666;
|
||||
color: var(--yj-text-tertiary, #666);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export class TrackInfo extends LitElement {
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -93,7 +93,7 @@ export class TrackInfo extends LitElement {
|
||||
|
||||
.secondary {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -101,7 +101,7 @@ export class TrackInfo extends LitElement {
|
||||
|
||||
.duration {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
flex-shrink: 0;
|
||||
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);
|
||||
padding: 8px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #666;
|
||||
color: var(--yj-text-primary, #fff);
|
||||
border-bottom: 1px solid var(--yj-text-tertiary, #666);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ export class TrackList extends LitElement implements SelectionHost {
|
||||
width: 1px;
|
||||
cursor: col-resize;
|
||||
pointer-events: auto;
|
||||
background-color: #444;
|
||||
background-color: var(--yj-border, #444);
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ export class TrackList extends LitElement implements SelectionHost {
|
||||
|
||||
.col-resize-handle:hover,
|
||||
.col-resize-handle.active {
|
||||
background-color: #6c757d;
|
||||
background-color: var(--yj-text-tertiary, #6c757d);
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
@@ -307,7 +307,7 @@ export class TrackList extends LitElement implements SelectionHost {
|
||||
grid-template-columns: var(--grid-cols, 1fr 1fr 80px);
|
||||
font-size: 12px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
border-bottom: 1px solid var(--yj-border-subtle, #333);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
@@ -324,23 +324,23 @@ export class TrackList extends LitElement implements SelectionHost {
|
||||
}
|
||||
|
||||
.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 {
|
||||
background-color: rgba(100, 160, 255, 0.15);
|
||||
background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15));
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: #ffd43b;
|
||||
color: var(--yj-accent, #ffd43b);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -363,8 +363,8 @@ export class TrackList extends LitElement implements SelectionHost {
|
||||
}
|
||||
|
||||
.context-menu-panel {
|
||||
background-color: #343a40;
|
||||
border: 1px solid #444;
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
border: 1px solid var(--yj-border, #444);
|
||||
border-radius: 6px;
|
||||
padding: 4px 0;
|
||||
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 {
|
||||
--wa-color-text-normal: #fff;
|
||||
--wa-color-text-normal: var(--yj-text-primary, #fff);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -48,6 +48,9 @@ export const Events = {
|
||||
PlaylistTracksChanged: "PlaylistTracksChanged",
|
||||
PlaylistsRestored: "PlaylistsRestored",
|
||||
|
||||
// Config events
|
||||
ThemeConfigChanged: "ThemeConfigChanged",
|
||||
|
||||
// Library events
|
||||
LibraryScanStarted: "LibraryScanStarted",
|
||||
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 type { QueueState, QueueTrack, RepeatMode } from './queue-store';
|
||||
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: {
|
||||
input: {
|
||||
main: "index.html",
|
||||
config: "src/pages/config/index.html",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+8
-3
@@ -1,22 +1,27 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {http} from '../models';
|
||||
import {context} from '../models';
|
||||
|
||||
export function GetLibraryDirectory():Promise<string>;
|
||||
|
||||
export function GetScanConcurrency():Promise<string>;
|
||||
|
||||
export function GetThemeAccentColor():Promise<string>;
|
||||
|
||||
export function GetThemeBackgroundShade():Promise<string>;
|
||||
|
||||
export function Load():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 SetLibraryDirectory(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>;
|
||||
|
||||
@@ -10,6 +10,14 @@ export function 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() {
|
||||
return window['go']['config']['Config']['Load']();
|
||||
}
|
||||
@@ -18,10 +26,6 @@ export function Save() {
|
||||
return window['go']['config']['Config']['Save']();
|
||||
}
|
||||
|
||||
export function ServeHTTP(arg1, arg2) {
|
||||
return window['go']['config']['Config']['ServeHTTP'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SetContext(arg1) {
|
||||
return window['go']['config']['Config']['SetContext'](arg1);
|
||||
}
|
||||
@@ -34,6 +38,14 @@ export function 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() {
|
||||
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 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 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/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/golang-cz/devslog v0.0.15
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/wailsapp/wails/v2 v2.10.2
|
||||
golang.org/x/image v0.12.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/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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