update ui

This commit is contained in:
2026-05-17 00:04:52 +02:00
parent a285dda742
commit 9a9275dae5
26 changed files with 618 additions and 34 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-CanNU2CV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DqwSTyl8.css">
<script type="module" crossorigin src="/assets/index-D7ESsBgL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-SAHpcFVS.css">
</head>
<body>
<div id="root"></div>
+46
View File
@@ -0,0 +1,46 @@
{
"hash": "da0aea58",
"configHash": "f821e5b8",
"lockfileHash": "5d6ef89d",
"browserHash": "9295d454",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "ef6552ca",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "d1244567",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "a6e059f6",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "eb926eaa",
"needsInterop": true
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "f7b551c0",
"needsInterop": true
}
},
"chunks": {
"chunk-373CG7ZK": {
"file": "chunk-373CG7ZK.js"
},
"chunk-REFQX4J5": {
"file": "chunk-REFQX4J5.js"
}
}
}
+534 -19
View File
@@ -3,27 +3,32 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #1e1e2e;
--surface: #313244;
--surface-hi: #45475a;
--border: #45475a;
--border-hi: #585b70;
--text: #cdd6f4;
--text-muted: #a6adc8;
--text-dim: #6c7086;
--accent: #cba6f7;
--accent-lo: rgba(203,166,247,0.16);
--accent-hi: #f5c2e7;
--green: #a6e3a1;
--green-lo: rgba(166,227,161,0.16);
--red: #f38ba8;
--yellow: #f9e2af;
--blue: #89b4fa;
--mauve: #cba6f7;
--overlay: #11111b;
--radius: 3px;
--bg: #111418;
--surface: #1b2026;
--surface-hi: #252c34;
--surface-soft: color-mix(in srgb, var(--surface) 58%, var(--bg));
--border: #2d3540;
--border-hi: #46515f;
--text: #e7edf3;
--text-muted: #9ba7b4;
--text-dim: #677482;
--accent: #44c2b4;
--accent-lo: rgba(68,194,180,0.15);
--accent-hi: #8de3da;
--accent-text: #071513;
--green: #79d28f;
--green-lo: rgba(121,210,143,0.16);
--red: #f07178;
--yellow: #e9c46a;
--blue: #7aa2f7;
--mauve: #b48ead;
--peach: #f4a261;
--base: #111418;
--overlay: #0b0e11;
--radius: 6px;
--ff: 'Syne', sans-serif;
--fm: 'JetBrains Mono', monospace;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
}
html, body, #root { height: 100%; }
@@ -1597,3 +1602,513 @@ body {
outline: none;
}
.settings-input--sm { width: 70px; }
/* ── Design refresh ───────────────────────────────────────────────────────── */
body {
background:
radial-gradient(circle at 18% 0%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 34%),
linear-gradient(180deg, color-mix(in srgb, var(--surface-hi) 32%, var(--bg)) 0%, var(--bg) 48%, color-mix(in srgb, var(--overlay) 68%, var(--bg)) 100%);
}
.app {
width: min(100%, 980px);
max-width: 980px;
min-height: 100%;
height: auto;
padding: 0 16px;
}
.app-header {
position: sticky;
top: 0;
z-index: 20;
gap: 22px;
padding: 18px 0 12px;
border-bottom: 1px solid color-mix(in srgb, var(--border-hi) 70%, transparent);
background: linear-gradient(180deg, color-mix(in srgb, var(--bg) 98%, transparent), color-mix(in srgb, var(--bg) 88%, transparent));
backdrop-filter: blur(14px);
}
.app-title {
min-width: 132px;
letter-spacing: 0.08em;
}
.app-title-main {
color: var(--text);
font-size: 17px;
}
.app-title-sub {
color: var(--accent);
font-size: 10px;
}
.tab-nav {
flex: 1;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
max-width: 560px;
}
.tab-btn {
border: 1px solid transparent;
border-radius: var(--radius);
color: var(--text-muted);
font-size: 12px;
letter-spacing: 0.06em;
padding: 9px 10px;
text-transform: none;
}
.tab-btn:hover {
background: rgba(255, 255, 255, 0.03);
border-color: var(--border);
}
.tab-btn--active {
background: var(--accent-lo);
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
color: var(--accent-hi);
}
.app-main {
padding: 16px 0 48px;
}
.panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.panel-section {
border: 1px solid var(--border);
border-radius: var(--radius);
background: linear-gradient(180deg, color-mix(in srgb, var(--surface-hi) 72%, transparent), color-mix(in srgb, var(--surface) 72%, transparent));
padding: 18px;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset;
}
.panel-section--meta {
padding: 12px 18px;
}
.panel-error,
.panel-empty {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-soft);
}
.section-header {
margin-bottom: 16px;
}
.section-label {
color: var(--text-dim);
font-size: 11px;
letter-spacing: 0.14em;
}
.section-value {
color: var(--text-muted);
}
.icon-btn,
.browser-icon-btn,
.browser-back-btn,
.settings-action-btn,
.recognize-btn,
.danger-btn,
.mute-btn,
.toggle-btn,
.transport-btn {
min-height: 32px;
}
.icon-btn,
.browser-icon-btn,
.browser-back-btn,
.settings-action-btn,
.recognize-btn,
.danger-btn,
.mute-btn,
.delete-btn,
.toggle-btn,
.transport-btn,
.playlist-item,
.cover-card,
.browser-mode-btn,
.album-back-btn,
.album-play-btn,
.album-play-dropdown button {
-webkit-tap-highlight-color: transparent;
}
.icon-btn:focus-visible,
.tab-btn:focus-visible,
.select:focus-visible,
.settings-input:focus-visible,
.browser-input:focus-visible,
.transport-btn:focus-visible,
.toggle-btn:focus-visible,
.browser-mode-btn:focus-visible,
.settings-action-btn:focus-visible {
outline: 2px solid color-mix(in srgb, var(--accent) 75%, transparent);
outline-offset: 2px;
}
.vol-slider,
.slider-track,
.browser-input,
.settings-input,
.select {
background: var(--surface-soft);
border-color: var(--border);
}
.vol-slider {
height: 38px;
}
.vol-fill,
.slider-fill,
.progress-fill {
background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 34%, transparent), var(--accent));
}
.now-playing {
gap: 16px;
padding: 2px 0 16px;
}
.now-cover {
width: 92px;
height: 92px;
flex-basis: 92px;
border-color: var(--border-hi);
box-shadow: var(--shadow);
}
.now-cover--empty {
box-shadow: none;
}
.track-title {
color: var(--text);
font-size: 22px;
line-height: 1.15;
margin-bottom: 8px;
}
.track-sub {
color: var(--text-muted);
line-height: 1.45;
}
.progress-bar {
height: 8px;
background: color-mix(in srgb, var(--overlay) 78%, var(--bg));
border: 1px solid var(--border);
border-radius: 999px;
margin-bottom: 8px;
}
.progress-fill {
border-radius: 999px;
}
.transport-panel {
gap: 14px;
}
.transport {
gap: 10px;
}
.transport-btn {
background: var(--surface-soft);
border-color: var(--border);
border-radius: var(--radius);
font-size: 17px;
}
.transport-btn:hover:not(:disabled) {
transform: translateY(-1px);
}
.transport-btn--play {
width: 76px;
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
color: var(--accent-hi);
}
.transport-btn--play.transport-btn--active {
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 28%, transparent), color-mix(in srgb, var(--accent) 12%, transparent));
}
.transport-volume {
flex-basis: 190px;
}
.toggle-btn {
width: 38px;
background: var(--surface-soft);
}
.toggle-btn--on,
.transport-btn--active,
.browser-mode-btn--active {
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent) inset;
}
.section-toggle {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-soft);
color: var(--text-muted);
padding: 10px 12px;
}
.playlist-list,
.browser {
margin-top: 10px;
}
.browser-mode-row {
gap: 8px;
padding: 0 0 10px;
}
.browser-mode-btn {
background: var(--surface-soft);
padding: 9px 10px;
letter-spacing: 0.08em;
}
.browser-search-row,
.browser-nav {
padding: 10px 0;
}
.browser-input {
min-height: 36px;
}
.browser-list {
min-height: min(66vh, 720px);
max-height: min(66vh, 720px);
padding-right: 4px;
}
.browser-row {
min-height: 42px;
padding: 7px 8px;
border-radius: var(--radius);
}
.browser-row:hover {
background: rgba(255, 255, 255, 0.035);
}
.cover-grid {
grid-template-columns: repeat(auto-fill, minmax(142px, 1fr));
gap: 14px;
}
.cover-card {
background: var(--surface-soft);
}
.cover-title {
min-height: 48px;
padding: 10px;
}
.queue-backdrop {
background: rgba(5, 7, 10, 0.68);
backdrop-filter: blur(3px);
}
.queue-drawer {
width: min(820px, calc(100% - 24px));
background: color-mix(in srgb, var(--surface-soft) 80%, var(--overlay));
border-color: var(--border-hi);
border-radius: 10px 10px 0 0;
}
.queue-item {
min-height: 48px;
}
.dsp-response {
background: color-mix(in srgb, var(--overlay) 78%, var(--bg));
}
.dsp-response svg {
height: 190px;
}
.dsp-response-fill {
fill: color-mix(in srgb, var(--accent) 12%, transparent);
}
.group-section {
background: var(--surface-soft);
border-color: var(--border);
}
.group-header,
.group-vol {
background: color-mix(in srgb, var(--surface-hi) 78%, transparent);
}
.client-row:hover {
background: rgba(255, 255, 255, 0.025);
}
.stream-row,
.settings-service-row,
.settings-row,
.meta-row {
min-height: 36px;
}
.stream-row,
.settings-service-row {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-soft);
padding: 9px 10px;
}
.snap-art {
border-color: var(--border-hi);
box-shadow: var(--shadow);
}
.snap-meta {
background: linear-gradient(to bottom, rgba(5, 7, 10, 0.78), rgba(5, 7, 10, 0.34), transparent);
}
.snap-title {
font-size: 20px;
}
.album-header {
padding: 8px 0;
}
.album-play-btn {
color: var(--accent-text);
}
.album-play-btn:hover {
transform: translateY(-1px);
}
.track-list {
max-height: min(54vh, 520px);
}
.disc-heading {
background: color-mix(in srgb, var(--surface-soft) 80%, var(--overlay));
}
.track-row {
min-height: 38px;
}
.settings-input {
min-height: 34px;
}
@media (min-width: 760px) {
.app-main > .panel {
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.panel-section:first-child .now-playing {
align-items: flex-start;
}
.panel-section:first-child .now-cover {
width: 124px;
height: 124px;
flex-basis: 124px;
}
}
@media (max-width: 640px) {
.app {
padding: 0 10px;
}
.app-header {
align-items: stretch;
flex-direction: column;
gap: 12px;
padding-top: 14px;
}
.app-title {
min-width: 0;
}
.tab-nav {
max-width: none;
}
.tab-btn {
font-size: 11px;
padding: 9px 6px;
}
.app-main {
padding-top: 10px;
}
.panel {
gap: 10px;
}
.panel-section {
padding: 14px 12px;
}
.now-cover {
width: 76px;
height: 76px;
flex-basis: 76px;
}
.track-title {
font-size: 18px;
}
.transport-volume {
flex-basis: 100%;
}
.browser-list {
min-height: 560px;
max-height: 560px;
}
.cover-grid {
grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
}
.settings-row {
align-items: stretch;
flex-direction: column;
}
.settings-input,
.settings-input--sm {
width: 100%;
}
}
+4 -3
View File
@@ -8,9 +8,9 @@ import './App.css';
const TABS = [
{ id: 'dsp', label: 'DSP' },
{ id: 'player', label: 'PLAYER' },
{ id: 'network', label: 'NETWORK' },
{ id: 'settings', label: 'SETTINGS' },
{ id: 'player', label: 'Player' },
{ id: 'network', label: 'Network' },
{ id: 'settings', label: 'Settings' },
];
export default function App() {
@@ -30,6 +30,7 @@ export default function App() {
key={t.id}
className={`tab-btn ${tab === t.id ? 'tab-btn--active' : ''}`}
onClick={() => setTab(t.id)}
aria-pressed={tab === t.id}
>
{t.label}
</button>
+10
View File
@@ -5,13 +5,17 @@ const DEFAULT_THEME = {
'--bg': '#1e1e2e',
'--surface': '#313244',
'--surface-hi': '#45475a',
'--surface-soft': '#272838',
'--border': '#45475a',
'--border-hi': '#585b70',
'--accent': '#cba6f7',
'--accent-lo': 'rgba(203,166,247,0.16)',
'--accent-hi': '#f5c2e7',
'--accent-text': '#11111b',
'--blue': '#89b4fa',
'--mauve': '#cba6f7',
'--base': '#1e1e2e',
'--overlay': '#11111b',
};
function clamp(value, min, max) {
@@ -74,21 +78,27 @@ function deriveTheme([r, g, b]) {
const accentHi = hslToRgb((hue + 28) % 360, clamp(sat * 0.86, 0.32, 0.72), 0.82);
const bg = hslToRgb(hue, clamp(sat * 0.22, 0.12, 0.28), 0.13);
const surface = hslToRgb(hue, clamp(sat * 0.24, 0.14, 0.32), 0.19);
const surfaceSoft = hslToRgb(hue, clamp(sat * 0.20, 0.12, 0.28), 0.16);
const surfaceHi = hslToRgb(hue, clamp(sat * 0.22, 0.12, 0.3), 0.27);
const border = hslToRgb(hue, clamp(sat * 0.20, 0.10, 0.28), 0.29);
const borderHi = hslToRgb(hue, clamp(sat * 0.22, 0.12, 0.32), 0.36);
const overlay = hslToRgb(hue, clamp(sat * 0.18, 0.10, 0.24), 0.08);
return {
'--bg': hex(bg),
'--surface': hex(surface),
'--surface-soft': hex(surfaceSoft),
'--surface-hi': hex(surfaceHi),
'--border': hex(border),
'--border-hi': hex(borderHi),
'--accent': hex(accent),
'--accent-lo': rgba(accent, 0.16),
'--accent-hi': hex(accentHi),
'--accent-text': '#0b0e11',
'--blue': hex(accent),
'--mauve': hex(accentHi),
'--base': hex(bg),
'--overlay': hex(overlay),
};
}
+1 -1
View File
@@ -14,7 +14,7 @@ config_file =
[audio]
mixer = software
mixer_volume =
output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! tee name=t ! queue leaky=2 ! filesink location=/audio/mopidy.fifo t. ! queue leaky=2 ! filesink location=/audio/cava.fifo
output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! tee name=t ! queue max-size-buffers=0 max-size-bytes=0 max-size-time=2000000000 ! filesink location=/audio/mopidy.fifo sync=true async=false t. ! queue leaky=2 max-size-buffers=0 max-size-bytes=0 max-size-time=200000000 ! filesink location=/audio/cava.fifo sync=true async=false
buffer_time =
[proxy]
+13 -2
View File
@@ -106,6 +106,7 @@ def _update_env_file(updates):
# ── Internal state ────────────────────────────────────────────────────────────
_rpc_id = itertools.count(1)
_cache = {}
_cache_lock = threading.Lock()
_cava_lock = threading.Lock()
_cava_process = None
_cava_thread = None
@@ -151,7 +152,14 @@ def rpc(url, method, params=None, timeout=8):
# ── Snapcast / Mopidy metadata ────────────────────────────────────────────────
def snapcast_status():
def snapcast_status(max_age=0.75):
cache_key = ("snapcast_status", effective_snapcast_url())
now = time.time()
with _cache_lock:
cached = _cache.get(cache_key)
if cached and now - cached["at"] <= max_age:
return cached["data"]
result = rpc(effective_snapcast_url(), "Server.GetStatus")
server = result.get("server", result)
groups = server.get("groups", [])
@@ -162,12 +170,15 @@ def snapcast_status():
)
active_stream_id = active_group.get("stream_id") if active_group else None
active_stream = next((s for s in streams if s.get("id") == active_stream_id), None)
return {
data = {
"group": active_group,
"stream": active_stream,
"stream_id": active_stream_id,
"streams": streams,
}
with _cache_lock:
_cache[cache_key] = {"at": now, "data": data}
return data
def mopidy_now_playing():
+2 -1
View File
@@ -25,7 +25,8 @@ ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 1704 1705 1780
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s \
CMD wget -qO- http://localhost:1780/jsonrpc \
CMD curl -fsS http://localhost:1780/jsonrpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"Server.GetStatus","id":1}' \
| grep -q '"id":1' || exit 1