From 14e9f2294e3dffe5900cb88ef3279daee5a3f2ff Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 25 Apr 2026 01:24:11 +0300 Subject: [PATCH] feat(ui): rebuild player view to match Studio Reference mockup Restructures the player tab DOM to actually look like the editorial mockup, not just inherit new fonts. The previous commit only swapped tokens & typography on the legacy Spotify-clone layout. DOM additions (all preserve existing JS-touched IDs): - Vinyl stage: rotating vinyl wrapping the existing #album-art as a circular center label; spins only when state=playing via CSS hook - SVG tonearm: pivots in/out based on data-playstate - Kicker line: copper italic mono header above the track title - Editorial 4-cell metadata grid: State / Source / Elapsed / Length - Decorative spectrum bars (30, CSS-only animation, paused when idle) - VU meter cluster: needle visual driven by volume %, alongside the preserved volume slider for a11y - Folio marks: top-left and top-right of the player container JS hooks (small, additive): - updatePlaybackState now sets :root[data-playstate] for CSS - progress tick mirrors timecode into meta-grid cells - volume update rotates the VU needle - folio-version mirrors the version label i18n: - new keys: player.kicker, player.modes, player.folio_*, meta.* - added to both en.json and ru.json Restored: media_server/static/redesign-mockup.html (Studio Reference visual reference; deleting it in the prior commit was a mistake). --- media_server/static/css/styles.css | 443 +++++- media_server/static/index.html | 162 ++- media_server/static/js/core.js | 4 + media_server/static/js/player.js | 10 + media_server/static/locales/en.json | 8 + media_server/static/locales/ru.json | 8 + media_server/static/redesign-mockup.html | 1671 ++++++++++++++++++++++ 7 files changed, 2216 insertions(+), 90 deletions(-) create mode 100644 media_server/static/redesign-mockup.html diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css index 968f872..fa479d7 100644 --- a/media_server/static/css/styles.css +++ b/media_server/static/css/styles.css @@ -4292,7 +4292,7 @@ header > div:first-child { } /* ═══════════════════════════════════════════════════════════════ - PLAYER VIEW — magazine spread + PLAYER VIEW — magazine spread with vinyl stage ═══════════════════════════════════════════════════════════════ */ .player-container { background: transparent; @@ -4300,71 +4300,226 @@ header > div:first-child { padding: 0; box-shadow: none; margin-top: 12px; + position: relative; } -.player-layout { +/* Folio marks at top corners of the player container */ +.player-container > .folio { + position: absolute; + top: -42px; + font-family: var(--mono); + font-size: 10px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--ink-faint); + z-index: 1; +} +.player-container > .folio.tl { left: 0; } +.player-container > .folio.tr { right: 0; } + +.player-layout, +.now-playing { display: grid; - grid-template-columns: minmax(280px, 480px) 1fr; + grid-template-columns: minmax(280px, 460px) 1fr; gap: 64px; align-items: start; + margin-top: 28px; } @media (max-width: 980px) { - .player-layout { + .player-layout, + .now-playing { grid-template-columns: 1fr; gap: 36px; } } -.album-art-container { +/* ─── Vinyl stage ──────────────────────────────────────────── */ +.album-art-container.vinyl-stage { position: relative; aspect-ratio: 1; width: 100%; max-width: none; - padding: 14px; - background: var(--bg-card); - border: 1px solid var(--rule-strong); + padding: 0; + background: transparent; + border: 0; + box-shadow: none; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; +} + +.album-art-container.vinyl-stage:hover { transform: none; } + +.vinyl-stage .vinyl { + position: relative; + width: 86%; + aspect-ratio: 1; + border-radius: 50%; + background: + radial-gradient(circle at 50% 50%, + #0a0907 0%, + #0a0907 18%, + #1a1611 18.3%, + #0a0907 18.6%, + #14110c 22%, + #0a0907 22.3%, + #14110c 26%, + #0a0907 26.3%, + #14110c 30%, + #0a0907 30.3%, + #14110c 34%, + #0a0907 34.3%, + #14110c 38%, + #0a0907 38.3%, + #14110c 42%, + #0a0907 42.3%, + #14110c 46%, + #0a0907 46.3%, + #1c1812 47%, + #0a0907 100%); box-shadow: - 0 1px 0 var(--bg-paper), - 0 28px 60px -20px rgba(0, 0, 0, 0.5), - 0 8px 20px -8px rgba(0, 0, 0, 0.35); - transition: transform 400ms var(--ease); + inset 0 0 60px rgba(0,0,0,0.7), + 0 30px 80px rgba(0,0,0,0.6), + 0 6px 20px rgba(0,0,0,0.5); + animation: sr-vinyl-spin 14s linear infinite; + animation-play-state: paused; } -.album-art-container:hover { transform: translateY(-2px); } - -#album-art-glow { - border-radius: 0; - filter: blur(40px) saturate(1.4); - opacity: 0.5; - inset: 14px; - width: auto; - height: auto; +:root[data-playstate="playing"] .vinyl-stage .vinyl { + animation-play-state: running; } -#album-art { - border-radius: 0; +.vinyl-stage .vinyl::before { + content: ""; + position: absolute; + inset: 12%; + border-radius: 50%; + background: + conic-gradient(from 0deg, + rgba(255,255,255,0.04) 0deg, + transparent 30deg, + rgba(255,255,255,0.06) 90deg, + transparent 150deg, + rgba(255,255,255,0.03) 210deg, + transparent 270deg, + rgba(255,255,255,0.05) 330deg, + transparent 360deg); + mix-blend-mode: screen; + pointer-events: none; +} + +.vinyl-stage .vinyl-label { + position: absolute; + inset: 28%; + border-radius: 50%; + overflow: hidden; + box-shadow: + inset 0 0 24px rgba(0,0,0,0.4), + 0 0 0 4px var(--bg-deep), + 0 0 0 5px var(--copper-lo); + background: var(--bg-card); + z-index: 1; +} + +.vinyl-stage .vinyl-label::after { + /* Spindle hole */ + content: ""; + position: absolute; + width: 8%; height: 8%; + top: 46%; left: 46%; + border-radius: 50%; + background: var(--bg-deep); + box-shadow: inset 0 1px 2px rgba(255,255,255,0.1); + z-index: 3; +} + +.vinyl-stage #album-art-glow { + position: absolute; + inset: -8%; + width: 116%; + height: 116%; + border-radius: 50%; + filter: blur(20px) saturate(1.4); + opacity: 0.55; + z-index: 0; + object-fit: cover; +} + +.vinyl-stage #album-art { + position: relative; width: 100%; height: 100%; object-fit: cover; display: block; + border-radius: 50%; + z-index: 2; } -.spectrogram-canvas { - bottom: 14px; - left: 14px; - right: 14px; - width: auto; +/* ─── Tonearm SVG ──────────────────────────────────────────── */ +.vinyl-stage .tonearm { + position: absolute; + top: -6%; + right: -4%; + width: 56%; + height: 56%; + pointer-events: none; + transform-origin: 88% 12%; + transform: rotate(-22deg); + transition: transform 1s var(--ease); + z-index: 3; + filter: drop-shadow(0 4px 12px rgba(0,0,0,0.5)); +} + +:root[data-playstate="playing"] .vinyl-stage .tonearm { + transform: rotate(0deg); +} + +@keyframes sr-vinyl-spin { to { transform: rotate(360deg); } } + +.vinyl-stage .spectrogram-canvas { + position: absolute; + bottom: -52px; + left: 0; + right: 0; + width: 100%; + height: 44px; border-radius: 0; - height: 56px; + opacity: 0.7; } /* ─── Player details (right column / masthead) ──────────────── */ -.player-details { +.player-details, +.track-masthead { gap: 0; - padding-top: 12px; + padding-top: 0; + display: flex; + flex-direction: column; } +/* Kicker — copper italic mono with hairlines */ +.track-masthead > .kicker { + font-family: var(--mono); + font-size: 10px; + letter-spacing: 0.32em; + text-transform: uppercase; + color: var(--copper); + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 22px; +} +.track-masthead > .kicker::before, +.track-masthead > .kicker::after { + content: ""; + height: 1px; + background: var(--copper); + opacity: 0.6; + flex: 0 0 24px; +} +.track-masthead > .kicker::after { flex: 1 0 auto; } + .track-info { margin-bottom: 0; position: relative; @@ -4399,27 +4554,225 @@ header > div:first-child { margin-bottom: 0; } -.playback-state { - margin-top: 22px; - padding: 12px 0; +/* Editorial 4-cell metadata grid (now houses State / Source / Elapsed / Length) */ +.meta-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + margin-top: 32px; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); +} +.meta-cell { + padding: 16px 18px 16px 0; + border-right: 1px solid var(--rule); + min-width: 0; +} +.meta-cell:last-child { border-right: 0; padding-right: 0; } +.meta-cell .meta-label { font-family: var(--mono); - font-size: 11px; + font-size: 9px; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ink-faint); + margin-bottom: 8px; +} +.meta-cell .meta-value { + font-family: var(--serif); + font-style: italic; + font-weight: 400; + font-size: 18px; + color: var(--ink); + font-variation-settings: 'opsz' 30; + display: flex; + align-items: center; + gap: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.meta-cell .meta-value.mono { + font-family: var(--mono); + font-style: normal; + font-size: 15px; + color: var(--ink); + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; +} +.meta-cell .meta-value .state-icon, +.meta-cell .meta-value .source-icon { + width: 14px; + height: 14px; + flex-shrink: 0; + color: var(--copper); +} +.meta-cell .source-value { font-size: 16px; } + +@media (max-width: 720px) { + .meta-grid { grid-template-columns: repeat(2, 1fr); } + .meta-cell:nth-child(2) { border-right: 0; } + .meta-cell:nth-child(-n+2) { border-bottom: 1px solid var(--rule); } +} + +/* Hide the legacy .playback-state container (its data is now in meta-grid) */ +.track-info > .playback-state { display: none; } + +/* Spectrum decorative bars */ +.spectrum { + display: flex; + align-items: flex-end; + gap: 3px; + height: 38px; + margin-top: 28px; + margin-bottom: 8px; +} +.spectrum span { + flex: 1; + background: linear-gradient(to top, var(--copper-lo), var(--copper-hi)); + opacity: 0.85; + border-radius: 99px 99px 0 0; + transform-origin: bottom; + animation: sr-spectrum 1.1s ease-in-out infinite; + animation-play-state: paused; +} +:root[data-playstate="playing"] .spectrum span { animation-play-state: running; } + +.spectrum span:nth-child(1) { animation-delay: -0.10s; height: 30%; } +.spectrum span:nth-child(2) { animation-delay: -0.45s; height: 60%; } +.spectrum span:nth-child(3) { animation-delay: -0.20s; height: 80%; } +.spectrum span:nth-child(4) { animation-delay: -0.55s; height: 50%; } +.spectrum span:nth-child(5) { animation-delay: -0.30s; height: 95%; } +.spectrum span:nth-child(6) { animation-delay: -0.05s; height: 70%; } +.spectrum span:nth-child(7) { animation-delay: -0.65s; height: 40%; } +.spectrum span:nth-child(8) { animation-delay: -0.25s; height: 85%; } +.spectrum span:nth-child(9) { animation-delay: -0.40s; height: 55%; } +.spectrum span:nth-child(10) { animation-delay: -0.10s; height: 75%; } +.spectrum span:nth-child(11) { animation-delay: -0.50s; height: 35%; } +.spectrum span:nth-child(12) { animation-delay: -0.15s; height: 90%; } +.spectrum span:nth-child(13) { animation-delay: -0.60s; height: 45%; } +.spectrum span:nth-child(14) { animation-delay: -0.30s; height: 65%; } +.spectrum span:nth-child(15) { animation-delay: -0.45s; height: 85%; } +.spectrum span:nth-child(16) { animation-delay: -0.20s; height: 55%; } +.spectrum span:nth-child(17) { animation-delay: -0.55s; height: 70%; } +.spectrum span:nth-child(18) { animation-delay: -0.10s; height: 30%; } +.spectrum span:nth-child(19) { animation-delay: -0.40s; height: 80%; } +.spectrum span:nth-child(20) { animation-delay: -0.25s; height: 60%; } +.spectrum span:nth-child(21) { animation-delay: -0.50s; height: 90%; } +.spectrum span:nth-child(22) { animation-delay: -0.15s; height: 50%; } +.spectrum span:nth-child(23) { animation-delay: -0.60s; height: 70%; } +.spectrum span:nth-child(24) { animation-delay: -0.30s; height: 40%; } +.spectrum span:nth-child(25) { animation-delay: -0.45s; height: 85%; } +.spectrum span:nth-child(26) { animation-delay: -0.20s; height: 55%; } +.spectrum span:nth-child(27) { animation-delay: -0.55s; height: 75%; } +.spectrum span:nth-child(28) { animation-delay: -0.10s; height: 35%; } +.spectrum span:nth-child(29) { animation-delay: -0.40s; height: 65%; } +.spectrum span:nth-child(30) { animation-delay: -0.25s; height: 95%; } + +@keyframes sr-spectrum { + 0%, 100% { transform: scaleY(0.4); } + 50% { transform: scaleY(1); } +} + +/* Transport — wraps progress + controls */ +.transport { + margin-top: 8px; +} + +.progress-row { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 18px; + align-items: center; + margin-bottom: 26px; +} +.progress-row .timecode { + font-family: var(--mono); + font-size: 12px; + color: var(--ink-mute); + letter-spacing: 0.06em; + font-variant-numeric: tabular-nums; +} +.progress-row .timecode.elapsed { color: var(--copper); font-weight: 500; } + +/* Override the legacy .progress-container layout when inside .transport */ +.transport .progress-container { + margin-top: 0; +} + +/* VU cluster (replaces freestanding volume container) */ +.vu-cluster { + margin-left: auto; + display: flex; + align-items: center; + gap: 14px; +} + +.vu-meter { + position: relative; + width: 130px; + height: 56px; + background: linear-gradient(180deg, #1a1610 0%, #0e0c08 100%); + border: 1px solid var(--rule-strong); + border-radius: 4px 4px 0 0; + overflow: hidden; + box-shadow: inset 0 2px 6px rgba(0,0,0,0.5), inset 0 0 30px rgba(224,128,56,0.08); +} +.vu-meter::before { + content: ""; + position: absolute; + inset: 0; + background: repeating-conic-gradient(from 195deg at 50% 100%, + transparent 0deg 4deg, + rgba(242, 235, 220, 0.08) 4deg 5deg, + transparent 5deg 9deg); +} +.vu-meter::after { + content: "VU"; + position: absolute; + bottom: 4px; left: 50%; + transform: translateX(-50%); + font-family: var(--mono); + font-size: 8px; + letter-spacing: 0.3em; + color: var(--ink-faint); +} +.vu-needle { + position: absolute; + bottom: 0; left: 50%; + width: 1.5px; + height: 88%; + background: linear-gradient(to top, var(--copper) 0%, var(--copper-hi) 70%, var(--ink) 100%); + transform-origin: bottom center; + transform: rotate(-22deg); + transition: transform 350ms var(--ease); + box-shadow: 0 0 8px var(--copper-glow); +} + +/* Volume container nested inside vu-cluster — compact stacked controls */ +.vu-cluster .volume-container { + margin-top: 0; + padding-top: 0; + border-top: 0; + flex-direction: column; + gap: 8px; + align-items: stretch; + min-width: 140px; +} +.vu-cluster .volume-container > .mute-btn { + align-self: flex-start; +} +.vu-cluster .volume-container > #volume-slider { + width: 100%; +} +.vu-cluster .volume-container > .volume-display { + text-align: right; + font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-mute); - gap: 10px; } - -.state-icon { - width: 14px; - height: 14px; - color: var(--copper); -} - -#playback-state { - color: var(--ink-soft); +.vu-cluster .volume-container > .volume-display::before { + content: "VOL · "; + color: var(--ink-faint); } /* ─── Progress (hairline editorial) ─────────────────────────── */ diff --git a/media_server/static/index.html b/media_server/static/index.html index a1bd75b..85984cb 100644 --- a/media_server/static/index.html +++ b/media_server/static/index.html @@ -153,66 +153,138 @@
-
-
- - Album Art + Now Spinning · v— + Vol. I — Studio Reference + +
+ + +
+
+
+ + Album Art +
+
+
-
+ +
+ +
Now Playing
+
No media playing
-
- - - - Idle +
+ + +
+
+
State
+
+ + + + Idle +
+
+
+
Source
+
+ + Unknown +
+
+
+
Elapsed
+
0:00
+
+
+
Length
+
0:00
-
-
- 0:00 - 0:00 + + + + +
+
+ 0:00 +
+
+
+ 0:00
-
-
+ +
+ + + + + +
+ +
+ + +
50%
+
+
-
- - - -
- -
- - -
50%
-
- +
- Unknown + + Modes +
+

Studio Reference

+
    +
  • Editorial hi-fi mockup
  • +
  • Fraunces · Geist · Geist Mono
  • +
  • Copper-on-charcoal · grain
  • +
  • Asymmetric magazine grid
  • +
  • Vinyl + tonearm + VU meter
  • +
  • Hover over cards & buttons
  • +
+ + +
+ + + № 0008 · v0.2.0 + Vol. I — APR · MMXXVI + + +
+
+ + Connected · Local 8765 +
+
+ Media Server + Studio Reference Edition +
+
+ + + + + + + +
+
+ + + + + +
+ + +
+
+
+
+ Kind of
Blue +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
Now Spinning · Track 03 of 05 · Spotify
+ +

So What — Take One

+ +
Kind of Blue · Columbia · 1959 · Remastered
+ + +
+
+
Bitrate
+
320 kbps
+
+
+
Format
+
FLAC · 24/96
+
+
+
Output
+
Studio Mon.
+
+
+
Genre
+
Modal Jazz
+
+
+ + +
+ + + + + + +
+ + +
+ +
+ 03:42 +
+
+
+ 09:22 +
+ +
+ + + + +
+
+
+
+
+ OUT −6 dB + VOL 72% +
+
+
+
+
+
+ + +
+
§ 03The Library
+
+
14 folders · 2,148 items
+
+ +
+
+ Music + / + Jazz + / + Miles Davis +
+
+ + + + +
+ +
+
+
+ A1 +
+
+
+
Kind of Blue
+
1959 · 5 tracks
+
+
+
+
+ A2 +
+
+
+
Sketches of Spain
+
1960 · 5 tracks
+
+
+
+
+ A3 +
+
+
+
Bitches Brew
+
1970 · 7 tracks
+
+
+
+
+ A4 +
+
+
+
In a Silent Way
+
1969 · 4 tracks
+
+
+
+
+ A5 +
+
+
+
Birth of the Cool
+
1957 · 11 tracks
+
+
+
+
+ A6 +
+
+
+
Porgy and Bess
+
1959 · 13 tracks
+
+
+
+
+ A7 +
+
+
+
Milestones
+
1958 · 7 tracks
+
+
+
+
+ A8 +
+
+
+
'Round About Midnight
+
1957 · 6 tracks
+
+
+
+ + +
+
§ 04Quick Access
+
+
Scripts · Shortcuts · Links
+
+ +
+ + + + + + + + +
+ + +
+
§ 05Settings
+
+
System · Audio · Folders · Callbacks
+
+ +
+ +
+ 5.01 +

Audio Output

+

Loopback device captured by the visualizer & VU meter. Auto-detection picks the system default.

+
+ Device + Realtek HD +
+
+ Status + CONNECTED +
+
+ Latency + 12 ms +
+ +
+ +
+ 5.02 +

Media Folders

+

Library roots scanned for browsing. Network shares show availability status.

+
+ Music + D:\Music +
+
+ Movies + \\NAS\Films +
+
+ Podcasts + UNREACHABLE +
+ +
+ +
+ 5.03 +

Scripts & Hooks

+

Custom commands that run on demand or trigger on playback events.

+
+ Scripts + 7 active +
+
+ Callbacks + 3 wired +
+
+ Last run + OK · 14:22 +
+ +
+ +
+ 5.04 +

System Health

+

Server diagnostics, websocket state, and update channel.

+
+ Version + v0.2.0 +
+
+ Uptime + 04 d · 11 h +
+
+ Update + UP TO DATE +
+ +
+ +
+ + + + +
+ + +
+
+
+
+
So What — Take One
+
Miles Davis · Kind of Blue
+
+
+
+ + + +
+
+ 03:42 / 09:22 +
+ +
+
+
+
+ + + +