added color/ theme settings, new config window
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user