diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css index 00af07b..a9c0454 100644 --- a/media_server/static/css/styles.css +++ b/media_server/static/css/styles.css @@ -4361,7 +4361,7 @@ header .brand-sub { } /* ═══════════════════════════════════════════════════════════════ - PLAYER VIEW — magazine spread with vinyl stage + PLAYER VIEW — verbatim from Studio Reference mockup ═══════════════════════════════════════════════════════════════ */ .player-container { background: transparent; @@ -4372,24 +4372,29 @@ header .brand-sub { position: relative; } -/* Folio marks at top corners of the player container */ -.player-container > .folio { +/* Visually hidden — kept in DOM for a11y/JS but invisible. */ +.now-playing .visually-hidden, +.player-container .visually-hidden { 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; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } -.player-container > .folio.tl { left: 0; } -.player-container > .folio.tr { right: 0; } -.player-layout, .now-playing { display: grid; - grid-template-columns: minmax(280px, 460px) 1fr; + grid-template-columns: minmax(320px, 520px) 1fr; + gap: 64px; + align-items: start; + margin-top: 28px; + position: relative; +} +.player-layout { + display: grid; + grid-template-columns: minmax(320px, 520px) 1fr; gap: 64px; align-items: start; margin-top: 28px; @@ -6281,13 +6286,12 @@ footer .separator { color: var(--ink-ghost); margin: 0 8px; } /* ─── Mobile breakpoint refinements ──────────────────────── */ @media (max-width: 720px) { - #track-title { font-size: 36px; } - .player-layout { gap: 24px; } - .album-art-container { padding: 10px; } - .album-art-container::before { font-size: 8px; bottom: -18px; } + #track-title, + .now-playing .track-title { font-size: 38px; } + .player-layout, .now-playing { gap: 28px; grid-template-columns: 1fr; } .controls { gap: 12px; } - .controls button { width: 42px; height: 42px; } - .controls button.primary { width: 56px; height: 56px; } + .now-playing .controls .btn-trans { width: 42px; height: 42px; } + .now-playing .controls .btn-trans.primary { width: 56px; height: 56px; } .mini-player { padding: 12px 16px; gap: 16px; } .tab-btn { padding: 14px 12px 12px; font-size: 12px; } .tab-btn.active { font-size: 15px; } @@ -6299,3 +6303,577 @@ footer .separator { color: var(--ink-ghost); margin: 0 8px; } .settings-section summary { font-size: 22px; } footer { font-size: 9px; } } + +/* ════════════════════════════════════════════════════════════════ + STUDIO REFERENCE — verbatim mockup snap for the player view. + Scoped to `.now-playing` so other tabs are unaffected. Wins over + earlier overrides through declaration order at equal specificity. + ════════════════════════════════════════════════════════════════ */ + +.now-playing { + display: grid; + grid-template-columns: minmax(320px, 520px) 1fr; + gap: 64px; + align-items: start; + margin-top: 28px; + position: relative; +} +@media (max-width: 980px) { + .now-playing { grid-template-columns: 1fr; gap: 40px; } +} + +/* ─── Vinyl + tonearm ─────────────────────────────────────── */ +.now-playing .vinyl-stage { + position: relative; + aspect-ratio: 1; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 0; + box-shadow: none; + padding: 0; + overflow: visible; + transform: none !important; +} + +.now-playing .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: + 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-snap-spin 14s linear infinite; + animation-play-state: paused; +} +:root[data-playstate="playing"] .now-playing .vinyl { + animation-play-state: running; +} +.now-playing .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 label = circular clip holding the actual album art */ +.now-playing .vinyl-label { + position: absolute; + inset: 28%; + border-radius: 50%; + overflow: hidden; + background: var(--bg-card); + 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); + z-index: 1; +} +.now-playing .vinyl-label::before { + /* 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; +} +.now-playing .vinyl-label #album-art-glow { + position: absolute; + inset: -10%; + width: 120%; + height: 120%; + border-radius: 50%; + filter: blur(22px) saturate(1.4); + opacity: 0.5; + z-index: 0; + object-fit: cover; +} +.now-playing .vinyl-label #album-art { + position: relative; + width: 100%; + height: 100%; + object-fit: cover; + display: block; + border-radius: 50%; + z-index: 2; +} + +/* Tonearm */ +.now-playing .tonearm { + position: absolute; + top: -8%; right: -4%; + width: 58%; height: 58%; + 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"] .now-playing .tonearm { + transform: rotate(0deg); +} + +@keyframes sr-snap-spin { to { transform: rotate(360deg); } } + +/* Spectrogram canvas hidden by default; toggle reveals it. */ +.now-playing .spectrogram-canvas { + position: absolute; + bottom: -52px; + left: 7%; right: 7%; + width: 86%; + height: 38px; + border-radius: 0; + opacity: 0; + transition: opacity 240ms var(--ease); + pointer-events: none; +} +.now-playing.visualizer-active .spectrogram-canvas, +body.visualizer-active .now-playing .spectrogram-canvas { + opacity: 0.6; +} + +/* ─── Track masthead ──────────────────────────────────────── */ +.now-playing .track-masthead { + display: flex; + flex-direction: column; + justify-content: center; + padding-top: 0; + gap: 0; +} + +.now-playing .kicker { + display: flex; + align-items: center; + gap: 14px; + font-family: var(--mono); + font-size: 10px; + letter-spacing: 0.32em; + text-transform: uppercase; + color: var(--copper); + margin-bottom: 22px; +} +.now-playing .kicker::before, +.now-playing .kicker::after { + content: ""; + height: 1px; + background: var(--copper); + opacity: 0.6; + flex: 0 0 24px; +} +.now-playing .kicker::after { flex: 1 0 auto; } + +.now-playing .track-title, +.now-playing #track-title { + font-family: var(--serif); + font-weight: 400; + font-size: clamp(42px, 5.6vw, 78px); + line-height: 0.96; + letter-spacing: -0.02em; + font-variation-settings: 'opsz' 144; + margin-bottom: 18px; + color: var(--ink); + margin-top: 0; +} +.now-playing .track-title em { + font-style: italic; + color: var(--copper-hi); +} + +.now-playing .track-byline, +.now-playing #artist { + font-family: var(--serif); + font-style: italic; + font-size: 22px; + font-weight: 300; + color: var(--ink-soft); + font-variation-settings: 'opsz' 60; + margin-bottom: 4px; + margin-top: 0; +} + +.now-playing .track-album, +.now-playing #album { + font-family: var(--sans); + font-size: 13px; + letter-spacing: 0.04em; + color: var(--ink-mute); + font-style: normal; + font-weight: 400; + margin-top: 0; +} + +/* ─── 2-cell metadata grid ────────────────────────────────── */ +.now-playing .meta-grid { + display: grid; + grid-template-columns: minmax(180px, auto) 1fr; + gap: 0; + margin-top: 36px; + border-top: 1px solid var(--rule); + border-bottom: 1px solid var(--rule); +} +.now-playing .meta-cell { + padding: 16px 24px 16px 0; + border-right: 1px solid var(--rule); + min-width: 0; +} +.now-playing .meta-cell:last-child { + border-right: 0; + padding-left: 24px; + padding-right: 0; +} +.now-playing .meta-cell .label { + font-family: var(--mono); + font-size: 9px; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ink-faint); + margin-bottom: 8px; +} +.now-playing .meta-cell .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: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.2; +} +.now-playing .meta-cell .value .state-icon, +.now-playing .meta-cell .value .source-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--copper); + fill: currentColor; +} + +/* ─── Spectrum bars ───────────────────────────────────────── */ +.now-playing .spectrum { + display: flex; + align-items: flex-end; + justify-content: center; + gap: 3px; + height: 38px; + margin: 36px auto 24px; + max-width: 480px; +} +.now-playing .spectrum span { + display: block; + width: 3px; + flex: 0 0 3px; + background: linear-gradient(to top, var(--copper-lo), var(--copper-hi)); + opacity: 0.85; + transform-origin: bottom; + border-radius: 99px 99px 0 0; + animation: sr-snap-bar 1.1s ease-in-out infinite; + animation-play-state: paused; +} +:root[data-playstate="playing"] .now-playing .spectrum span { + animation-play-state: running; +} +.now-playing .spectrum span:nth-child(1) { animation-delay: -0.10s; height: 30%; } +.now-playing .spectrum span:nth-child(2) { animation-delay: -0.45s; height: 60%; } +.now-playing .spectrum span:nth-child(3) { animation-delay: -0.20s; height: 80%; } +.now-playing .spectrum span:nth-child(4) { animation-delay: -0.55s; height: 50%; } +.now-playing .spectrum span:nth-child(5) { animation-delay: -0.30s; height: 95%; } +.now-playing .spectrum span:nth-child(6) { animation-delay: -0.05s; height: 70%; } +.now-playing .spectrum span:nth-child(7) { animation-delay: -0.65s; height: 40%; } +.now-playing .spectrum span:nth-child(8) { animation-delay: -0.25s; height: 85%; } +.now-playing .spectrum span:nth-child(9) { animation-delay: -0.40s; height: 55%; } +.now-playing .spectrum span:nth-child(10) { animation-delay: -0.10s; height: 75%; } +.now-playing .spectrum span:nth-child(11) { animation-delay: -0.50s; height: 35%; } +.now-playing .spectrum span:nth-child(12) { animation-delay: -0.15s; height: 90%; } +.now-playing .spectrum span:nth-child(13) { animation-delay: -0.60s; height: 45%; } +.now-playing .spectrum span:nth-child(14) { animation-delay: -0.30s; height: 65%; } +.now-playing .spectrum span:nth-child(15) { animation-delay: -0.45s; height: 85%; } +.now-playing .spectrum span:nth-child(16) { animation-delay: -0.20s; height: 55%; } +.now-playing .spectrum span:nth-child(17) { animation-delay: -0.55s; height: 70%; } +.now-playing .spectrum span:nth-child(18) { animation-delay: -0.10s; height: 30%; } +.now-playing .spectrum span:nth-child(19) { animation-delay: -0.40s; height: 80%; } +.now-playing .spectrum span:nth-child(20) { animation-delay: -0.25s; height: 60%; } +.now-playing .spectrum span:nth-child(21) { animation-delay: -0.50s; height: 90%; } +.now-playing .spectrum span:nth-child(22) { animation-delay: -0.15s; height: 50%; } +.now-playing .spectrum span:nth-child(23) { animation-delay: -0.60s; height: 70%; } +.now-playing .spectrum span:nth-child(24) { animation-delay: -0.30s; height: 40%; } +.now-playing .spectrum span:nth-child(25) { animation-delay: -0.45s; height: 85%; } +.now-playing .spectrum span:nth-child(26) { animation-delay: -0.20s; height: 55%; } +.now-playing .spectrum span:nth-child(27) { animation-delay: -0.55s; height: 75%; } +.now-playing .spectrum span:nth-child(28) { animation-delay: -0.10s; height: 35%; } +.now-playing .spectrum span:nth-child(29) { animation-delay: -0.40s; height: 65%; } +.now-playing .spectrum span:nth-child(30) { animation-delay: -0.25s; height: 95%; } + +@keyframes sr-snap-bar { + 0%, 100% { transform: scaleY(0.4); } + 50% { transform: scaleY(1); } +} + +/* ─── Transport ───────────────────────────────────────────── */ +.now-playing .transport { margin-top: 0; } + +.now-playing .progress-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 18px; + margin-bottom: 28px; +} +.now-playing .timecode { + font-family: var(--mono); + font-size: 12px; + color: var(--ink-mute); + letter-spacing: 0.06em; + font-variant-numeric: tabular-nums; + line-height: 1; +} +.now-playing .timecode.elapsed { color: var(--copper); font-weight: 500; } + +.now-playing .progress-track, +.now-playing .progress-bar { + height: 2px; + width: 100%; + background: var(--rule-strong); + position: relative; + cursor: pointer; + border-radius: 0; + overflow: visible; + transform: none !important; + transition: background 200ms var(--ease); + min-width: 0; + margin: 0; +} +.now-playing .progress-bar:hover, +.now-playing .progress-bar.dragging { + transform: none !important; + background: var(--rule-strong); +} +.now-playing .progress-fill { + position: absolute; + left: 0; top: 0; + height: 100%; + background: var(--copper); + box-shadow: 0 0 12px var(--copper-glow); + border-radius: 0; + width: 0; + transition: width 0.1s linear; +} +.now-playing .progress-fill::after { + content: ""; + position: absolute; + right: -5px; + top: 50%; + transform: translateY(-50%) scale(1) !important; + width: 10px; height: 10px; + background: var(--copper); + border-radius: 50%; + box-shadow: 0 0 14px var(--copper-glow), 0 0 0 4px rgba(224, 128, 56, 0.12); + transition: none; +} + +/* ─── Controls + VU cluster ───────────────────────────────── */ +.now-playing .controls { + display: flex; + align-items: center; + gap: 18px; + margin-top: 0; + justify-content: flex-start; +} + +.now-playing .btn-trans { + background: transparent; + border: 1px solid var(--rule-strong); + color: var(--ink-soft); + width: 48px; height: 48px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 50%; + padding: 0; + transition: all 200ms var(--ease); +} +.now-playing .btn-trans:hover { + border-color: var(--copper); + color: var(--copper); + background: rgba(224, 128, 56, 0.06); +} +.now-playing .btn-trans:disabled { + opacity: 0.35; + cursor: not-allowed; + border-color: var(--rule-strong); + color: var(--ink-mute); + background: transparent; +} +.now-playing .btn-trans.primary { + width: 64px; height: 64px; + background: var(--ink); + color: var(--bg-deep); + border-color: var(--ink); + box-shadow: 0 8px 28px rgba(0,0,0,0.45); +} +.now-playing .btn-trans.primary:hover { + background: var(--copper); + border-color: var(--copper); + color: var(--bg-deep); + transform: scale(1.04); + box-shadow: 0 8px 28px var(--copper-glow); +} +.now-playing .btn-trans svg { + width: 20px; height: 20px; + fill: currentColor; +} +.now-playing .btn-trans.primary svg { + width: 24px; height: 24px; +} + +/* VU cluster — display-only, click toggles mute */ +.now-playing .vu-cluster { + margin-left: auto; + display: flex; + align-items: center; + gap: 16px; + cursor: pointer; + user-select: none; + transition: opacity 200ms var(--ease); +} +.now-playing .vu-cluster:hover { opacity: 0.85; } +.now-playing .vu-cluster:focus-visible { + outline: 1px solid var(--copper); + outline-offset: 4px; +} +.now-playing .vu-cluster.muted .vu-needle { + background: linear-gradient(to top, var(--rust) 0%, var(--ink-mute) 100%); + box-shadow: 0 0 8px rgba(194, 85, 63, 0.4); +} +.now-playing .vu-cluster.muted .vu-readout strong { color: var(--rust); } + +.now-playing .vu-meter { + position: relative; + width: 140px; + height: 60px; + 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); + flex-shrink: 0; +} +.now-playing .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); +} +.now-playing .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); +} +.now-playing .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); +} + +.now-playing .vu-readout { + font-family: var(--mono); + font-size: 11px; + color: var(--ink-mute); + letter-spacing: 0.06em; + font-variant-numeric: tabular-nums; + display: flex; + flex-direction: column; + gap: 4px; + line-height: 1.2; + white-space: nowrap; +} +.now-playing .vu-readout strong { + color: var(--copper); + font-weight: 400; +} + +/* Mobile VU cluster: stack below controls */ +@media (max-width: 720px) { + .now-playing .controls { flex-wrap: wrap; } + .now-playing .vu-cluster { + margin-left: 0; + width: 100%; + justify-content: space-between; + margin-top: 12px; + } + .now-playing .vu-meter { width: 110px; height: 50px; } + .now-playing .meta-grid { + grid-template-columns: 1fr; + } + .now-playing .meta-cell { + border-right: 0; + border-bottom: 1px solid var(--rule); + padding: 12px 0; + } + .now-playing .meta-cell:last-child { + border-bottom: 0; + padding-left: 0; + } +} diff --git a/media_server/static/index.html b/media_server/static/index.html index a6fa38c..ab19ae9 100644 --- a/media_server/static/index.html +++ b/media_server/static/index.html @@ -157,10 +157,10 @@
-
+
- -
+ +
@@ -188,21 +188,19 @@
-
+
Now Playing
-
-
No media playing
-
-
-
+

No media playing

+ +
- +
-
State
-
+
State
+
@@ -210,8 +208,8 @@
-
Source
-
+
Source
+
Unknown
@@ -228,64 +226,55 @@
- +
-
+
0:00 -
+
0:00
- - - - -
+
-
- - -
50%
+
+ OUT SYS + VOL 50%
- -
- - Modes - -
- -
+ +
+ + +
50%
+
-
+
diff --git a/media_server/static/js/player.js b/media_server/static/js/player.js index 99a0694..261b0bb 100644 --- a/media_server/static/js/player.js +++ b/media_server/static/js/player.js @@ -685,6 +685,11 @@ export function updateUI(status) { const deg = -45 + (status.volume / 100) * 90; needle.style.transform = `rotate(${deg}deg)`; } + // Editorial VU readout: VOL XX% / OUT (SYS or MUTED) + const vuVol = document.getElementById('vu-vol'); + if (vuVol) vuVol.textContent = `${status.volume}%`; + const vuOut = document.getElementById('vu-out'); + if (vuOut) vuOut.textContent = status.muted ? 'MUTE' : 'SYS'; } updateMuteIcon(status.muted); @@ -788,4 +793,8 @@ function updateMuteIcon(muted) { const path = muted ? SVG_MUTED : SVG_UNMUTED; dom.muteIcon.innerHTML = path; dom.miniMuteIcon.innerHTML = path; + const vuOut = document.getElementById('vu-out'); + if (vuOut) vuOut.textContent = muted ? 'MUTE' : 'SYS'; + const cluster = document.querySelector('.now-playing .vu-cluster'); + if (cluster) cluster.classList.toggle('muted', muted); }