added color/ theme settings, new config window

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