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).
This commit is contained in:
@@ -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) ─────────────────────────── */
|
||||
|
||||
+117
-45
@@ -153,66 +153,138 @@
|
||||
</div>
|
||||
|
||||
<div class="player-container" data-tab-content="player" role="tabpanel" id="panel-player">
|
||||
<div class="player-layout">
|
||||
<div class="album-art-container">
|
||||
<img id="album-art-glow" class="album-art-glow" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E" alt="" aria-hidden="true">
|
||||
<img id="album-art" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E" alt="Album Art">
|
||||
<span class="folio tl"><span data-i18n="player.folio_left">Now Spinning</span> · <span id="folio-version">v—</span></span>
|
||||
<span class="folio tr" data-i18n="player.folio_right">Vol. I — Studio Reference</span>
|
||||
|
||||
<div class="player-layout now-playing">
|
||||
|
||||
<!-- Vinyl stage with album art as label -->
|
||||
<div class="album-art-container vinyl-stage">
|
||||
<div class="vinyl">
|
||||
<div class="vinyl-label">
|
||||
<img id="album-art-glow" class="album-art-glow" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E" alt="" aria-hidden="true">
|
||||
<img id="album-art" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E" alt="Album Art">
|
||||
</div>
|
||||
</div>
|
||||
<svg class="tonearm" viewBox="0 0 200 200" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="armGrad" x1="0" x2="1">
|
||||
<stop offset="0" stop-color="#3a3528"/>
|
||||
<stop offset="0.5" stop-color="#9C937F"/>
|
||||
<stop offset="1" stop-color="#5C5447"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="176" cy="24" r="14" fill="#1a1611" stroke="#3A3528" stroke-width="1"/>
|
||||
<circle cx="176" cy="24" r="6" fill="#3A3528"/>
|
||||
<circle cx="176" cy="24" r="2" fill="#E08038"/>
|
||||
<line x1="176" y1="24" x2="64" y2="136" stroke="url(#armGrad)" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<rect x="180" y="14" width="14" height="20" fill="#26211A" stroke="#3A3528"/>
|
||||
<rect x="56" y="128" width="22" height="18" rx="2" fill="#26211A" stroke="#3A3528" transform="rotate(-45 67 137)"/>
|
||||
<circle cx="62" cy="138" r="3" fill="#E08038" opacity="0.8"/>
|
||||
<circle cx="62" cy="138" r="6" fill="none" stroke="#E08038" stroke-width="0.5" opacity="0.4"/>
|
||||
</svg>
|
||||
<canvas id="spectrogram-canvas" class="spectrogram-canvas" width="300" height="64"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="player-details">
|
||||
<!-- Track masthead -->
|
||||
<div class="player-details track-masthead">
|
||||
|
||||
<div class="kicker"><span data-i18n="player.kicker">Now Playing</span></div>
|
||||
|
||||
<div class="track-info">
|
||||
<div id="track-title" data-i18n="player.no_media">No media playing</div>
|
||||
<div id="artist"></div>
|
||||
<div id="album"></div>
|
||||
<div class="playback-state">
|
||||
<svg class="state-icon" id="state-icon" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>
|
||||
</svg>
|
||||
<span id="playback-state" data-i18n="state.idle">Idle</span>
|
||||
</div>
|
||||
|
||||
<!-- Editorial metadata grid (4 cells) -->
|
||||
<div class="meta-grid">
|
||||
<div class="meta-cell">
|
||||
<div class="meta-label" data-i18n="meta.state">State</div>
|
||||
<div class="meta-value">
|
||||
<svg class="state-icon" id="state-icon" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>
|
||||
</svg>
|
||||
<span id="playback-state" data-i18n="state.idle">Idle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-cell">
|
||||
<div class="meta-label" data-i18n="meta.source">Source</div>
|
||||
<div class="meta-value source-value">
|
||||
<span class="source-icon" id="sourceIcon"></span>
|
||||
<span id="source" data-i18n="player.unknown_source">Unknown</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-cell">
|
||||
<div class="meta-label" data-i18n="meta.elapsed">Elapsed</div>
|
||||
<div class="meta-value mono" id="meta-elapsed">0:00</div>
|
||||
</div>
|
||||
<div class="meta-cell">
|
||||
<div class="meta-label" data-i18n="meta.length">Length</div>
|
||||
<div class="meta-value mono" id="meta-length">0:00</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-container">
|
||||
<div class="time-display">
|
||||
<span id="current-time">0:00</span>
|
||||
<span id="total-time">0:00</span>
|
||||
<!-- Decorative spectrum bars -->
|
||||
<div class="spectrum" aria-hidden="true">
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<!-- Transport: progress + controls + VU cluster -->
|
||||
<div class="transport">
|
||||
<div class="progress-container progress-row">
|
||||
<span class="timecode elapsed" id="current-time">0:00</span>
|
||||
<div class="progress-bar progress-track" id="progress-bar" data-duration="0" role="slider" aria-label="Playback position" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
<span class="timecode" id="total-time">0:00</span>
|
||||
</div>
|
||||
<div class="progress-bar" id="progress-bar" data-duration="0" role="slider" aria-label="Playback position" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
|
||||
<svg viewBox="0 0 24 24" id="play-pause-icon">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- VU cluster: needle visual + slider + readout -->
|
||||
<div class="vu-cluster">
|
||||
<div class="vu-meter" aria-hidden="true">
|
||||
<div class="vu-needle" id="vuNeedle"></div>
|
||||
</div>
|
||||
<div class="volume-container">
|
||||
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
|
||||
<svg viewBox="0 0 24 24" id="mute-icon">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="range" id="volume-slider" min="0" max="100" value="50" aria-label="Volume">
|
||||
<div class="volume-display" id="volume-display">50%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
|
||||
<svg viewBox="0 0 24 24" id="play-pause-icon">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="volume-container">
|
||||
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
|
||||
<svg viewBox="0 0 24 24" id="mute-icon">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="range" id="volume-slider" min="0" max="100" value="50" aria-label="Volume">
|
||||
<div class="volume-display" id="volume-display">50%</div>
|
||||
</div>
|
||||
|
||||
<!-- Player toggles -->
|
||||
<div class="source-info">
|
||||
<span class="source-label"><span class="source-icon" id="sourceIcon"></span><span id="source" data-i18n="player.unknown_source">Unknown</span></span>
|
||||
<span class="source-label">
|
||||
<span class="vinyl-mode-label" data-i18n="player.modes">Modes</span>
|
||||
</span>
|
||||
<div class="player-toggles">
|
||||
<button class="vinyl-toggle-btn" onclick="toggleVinylMode()" id="vinylToggle" data-i18n-title="player.vinyl" title="Vinyl mode">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="3" fill="currentColor"/><circle cx="12" cy="12" r="6.5" fill="none" stroke="currentColor" stroke-width="0.5" opacity="0.5"/></svg>
|
||||
|
||||
@@ -116,6 +116,8 @@ export function cacheDom() {
|
||||
dom.progressFill = document.getElementById('progress-fill');
|
||||
dom.currentTime = document.getElementById('current-time');
|
||||
dom.totalTime = document.getElementById('total-time');
|
||||
dom.metaElapsed = document.getElementById('meta-elapsed');
|
||||
dom.metaLength = document.getElementById('meta-length');
|
||||
dom.progressBar = document.getElementById('progress-bar');
|
||||
dom.miniProgressFill = document.getElementById('mini-progress-fill');
|
||||
dom.miniCurrentTime = document.getElementById('mini-current-time');
|
||||
@@ -317,6 +319,8 @@ export async function fetchVersion() {
|
||||
const label = document.getElementById('version-label');
|
||||
if (data.version) {
|
||||
label.textContent = `v${data.version}`;
|
||||
const folioVersion = document.getElementById('folio-version');
|
||||
if (folioVersion) folioVersion.textContent = `v${data.version}`;
|
||||
}
|
||||
if (data.update_available) {
|
||||
showUpdateBanner(data.update_available);
|
||||
|
||||
@@ -677,6 +677,12 @@ export function updateUI(status) {
|
||||
dom.volumeDisplay.textContent = `${status.volume}%`;
|
||||
dom.miniVolumeSlider.value = status.volume;
|
||||
dom.miniVolumeDisplay.textContent = `${status.volume}%`;
|
||||
// VU needle: map 0-100 volume to -45deg..+45deg rotation.
|
||||
const needle = document.getElementById('vuNeedle');
|
||||
if (needle) {
|
||||
const deg = -45 + (status.volume / 100) * 90;
|
||||
needle.style.transform = `rotate(${deg}deg)`;
|
||||
}
|
||||
}
|
||||
|
||||
updateMuteIcon(status.muted);
|
||||
@@ -700,6 +706,8 @@ export function updateUI(status) {
|
||||
|
||||
export function updatePlaybackState(state) {
|
||||
setCurrentPlayState(state);
|
||||
// Expose state to CSS so tonearm / vinyl spin can react.
|
||||
document.documentElement.dataset.playstate = state;
|
||||
switch(state) {
|
||||
case 'playing':
|
||||
dom.playbackState.textContent = t('state.playing');
|
||||
@@ -739,6 +747,8 @@ function updateProgress(position, duration) {
|
||||
dom.progressFill.style.width = widthStr;
|
||||
dom.currentTime.textContent = currentStr;
|
||||
dom.totalTime.textContent = totalStr;
|
||||
if (dom.metaElapsed) dom.metaElapsed.textContent = currentStr;
|
||||
if (dom.metaLength) dom.metaLength.textContent = totalStr;
|
||||
dom.progressBar.dataset.duration = duration;
|
||||
dom.progressBar.setAttribute('aria-valuenow', posRound);
|
||||
dom.progressBar.setAttribute('aria-valuemax', durRound);
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
"player.status.connected": "Connected",
|
||||
"player.status.disconnected": "Disconnected",
|
||||
"player.no_media": "No media playing",
|
||||
"player.kicker": "Now Playing",
|
||||
"player.modes": "Modes",
|
||||
"player.folio_left": "Now Spinning",
|
||||
"player.folio_right": "Vol. I — Studio Reference",
|
||||
"meta.state": "State",
|
||||
"meta.source": "Source",
|
||||
"meta.elapsed": "Elapsed",
|
||||
"meta.length": "Length",
|
||||
"player.title_unavailable": "Title unavailable",
|
||||
"player.source": "Source:",
|
||||
"player.unknown_source": "Unknown",
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
"player.status.connected": "Подключено",
|
||||
"player.status.disconnected": "Отключено",
|
||||
"player.no_media": "Медиа не воспроизводится",
|
||||
"player.kicker": "Сейчас играет",
|
||||
"player.modes": "Режимы",
|
||||
"player.folio_left": "Сейчас играет",
|
||||
"player.folio_right": "Том I — Studio Reference",
|
||||
"meta.state": "Состояние",
|
||||
"meta.source": "Источник",
|
||||
"meta.elapsed": "Прошло",
|
||||
"meta.length": "Длина",
|
||||
"player.title_unavailable": "Название недоступно",
|
||||
"player.source": "Источник:",
|
||||
"player.unknown_source": "Неизвестно",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user