mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
feat(fullscreen-player): rebuilt static fullscreen player (#1001)
* feat(player): static media-center fullscreen player (v1) New lean fullscreen player: sharp full-bleed background (artist photo, cover fallback), no blur / no continuous animations. Bottom-left big cover bottom- flush with a full-width semi-transparent text bar (title with queue position, artist, year/genre + rating stars, next-up); control row of transport · center time · actions; full-width seekbar; live clock. Only the seekbar, time readout and clock update at runtime, each owning its state (no per-tick re-render). Reuses FsSeekbar/FsPlayBtn, the cover/artist hooks, idle-fade and queue helpers. Wired in AppShell; old player kept for A/B. * feat(fullscreen-player): true waveform seekbar instead of thin bar Replace FsSeekbar with the real WaveformSeek (cucadmuh's idea). The taller canvas grows the bottom cluster upward, shifting the info/control rows up. Height clamped to 32-52px. * feat(fullscreen-player): up-next popover + control-bar styling - Queue button opens a semi-transparent 'Up next' popover anchored bottom right; clicking a row jumps to that queue item. - Larger bottom-right action buttons. - Control row gets its own darker semi-transparent bar (touches the info bar above) for contrast; play button matches the plain transport buttons (no white circle, same size). * feat(fullscreen-player): high-res (2000px) background cover Fetch the full-screen background cover at the 2000px tier via the existing on-demand fullRes path (same getCoverArt fetch, saved as a high-res WebP outside the backfill pipeline) instead of the low-res 500px pipeline tier. usePlaybackCoverArt now forwards a fullRes option to useCoverArt. * feat(fullscreen-player): scrolling lyrics overlay + control-bar polish - Lyrics button next to Queue toggles a centered, dark semi-transparent scrolling-lyrics overlay (reuses FsLyricsApple). - Control bar darkened to match the lyrics overlay (0.88). - Close button sized down to harmonize with the clock. * feat(fullscreen-player): make rating stars clickable The rating stars next to the year were display-only. Wire them to queueSongRating (same path as the player bar / context menu): click to set, click the current value to clear, with a hover preview. Keeps the lucide outline look to match the rest of the control bar. * fix(fullscreen-player): stable, high-res album cover The foreground cover was keyed per track, so Navidrome's per-track `mf-<id>` coverArt re-triggered the distinct-disc heuristic and reloaded the cover on every song change within the same album. Key it on albumId (via useAlbumCoverRef) so it stays put while the album is unchanged. It also used the low-res tier; reuse the fullRes 2000px cover already fetched for the background so the foreground is crisp and both share a single fetch/decode. * refactor(fullscreen-player): remove the old player and its settings The static rebuild is now the only fullscreen player. Delete the old FullscreenPlayer component, its old-only parts (FsArt, FsPortrait, FsSeekbar, FsLyricsRail, FsLyricsMenu, useFsDynamicAccent) and its test. Remove the now-orphaned settings (showFullscreenLyrics, fsLyricsStyle, showFsArtistPortrait, fsPortraitDim) along with the Appearance 'Fullscreen player' section and its search entry. The new player always shows the artist photo with cover fallback and has its own lyrics toggle. Shared building blocks used by the static player (FsLyricsApple, FsPlayBtn, FsClock, FsTimeReadout, FsQueueModal, useFsArtistPortrait) are kept. * i18n(fullscreen-player): translate the new player's strings Wire the static player's previously English-only strings through i18n across all 9 locales: now-playing label, track position, up-next label, queue/lyrics/shuffle controls, and the queue overlay (title, empty, close). Reuses existing queue.title / common.close keys; adds six new keys to the player namespace. * docs(changelog): note fullscreen player rebuild (#1001)
This commit is contained in:
committed by
GitHub
parent
c674e4515b
commit
dc4eef1a97
@@ -0,0 +1,512 @@
|
||||
/* ── Static fullscreen player (lean, media-center layout) ──────────────────
|
||||
No backdrop-filter, no blur, no infinite animations. Background is a sharp
|
||||
image; the darkening gradient + vignette are plain static overlays. Only the
|
||||
seekbar and clock update at runtime, and they own their state. */
|
||||
|
||||
.fsp {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: #06060e;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
--fsp-cover-size: clamp(150px, 24vh, 250px);
|
||||
}
|
||||
|
||||
.fsp[data-idle='true'] {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
/* ── Background ── */
|
||||
.fsp-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* Bias toward the upper third so faces survive a 16:9 crop of square art. */
|
||||
object-position: center 30%;
|
||||
}
|
||||
|
||||
.fsp-bg--empty {
|
||||
background: radial-gradient(circle at 50% 35%, #1a1a2e, #06060e 70%);
|
||||
}
|
||||
|
||||
/* Static text-contrast scrim + edge vignette — baked look, no runtime effect. */
|
||||
.fsp-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(to top, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 38%),
|
||||
linear-gradient(to right, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0) 55%),
|
||||
linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 22%);
|
||||
}
|
||||
|
||||
.fsp-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(ellipse 75% 75% at 50% 45%, transparent 55%, rgba(0, 0, 0, 0.55) 100%);
|
||||
}
|
||||
|
||||
/* ── Top bar ── */
|
||||
.fsp-top {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
padding-right: calc(var(--space-6) + 52px); /* leave room for the close button */
|
||||
}
|
||||
|
||||
.fsp-nowplaying {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.fsp-nowplaying-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.fsp-nowplaying-pos {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.fsp-clock {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Close (fades when idle) ── */
|
||||
.fsp-close {
|
||||
position: absolute;
|
||||
top: var(--space-4);
|
||||
right: var(--space-4);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-base), background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.fsp[data-idle='false'] .fsp-close,
|
||||
.fsp:hover .fsp-close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fsp-close:hover {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Bottom cluster ── */
|
||||
.fsp-foot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding: 0 var(--space-6) var(--space-5);
|
||||
/* Soft backing for the control row only — the text has its own hard bar. */
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.18) 26%, rgba(0, 0, 0, 0) 52%);
|
||||
}
|
||||
|
||||
/* The black bar: hugs the text + cover with equal padding above and below.
|
||||
The cover is bottom-aligned to the text and pokes above the bar's top. */
|
||||
.fsp-info-row {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
/* Span the full width (cancel the foot's side padding); text stays inset. */
|
||||
margin: 0 calc(-1 * var(--space-6));
|
||||
padding: var(--space-4) var(--space-6);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Big cover, bottom-flush with the text block; its top pokes above the bar. */
|
||||
.fsp-cover {
|
||||
position: absolute;
|
||||
left: var(--space-6);
|
||||
bottom: var(--space-4);
|
||||
z-index: 1;
|
||||
width: var(--fsp-cover-size);
|
||||
height: var(--fsp-cover-size);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.fsp-cover-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.fsp-cover-img--empty {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.fsp-info-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
/* Clear the overlapping cover on the left. */
|
||||
padding-left: calc(var(--fsp-cover-size) + var(--space-5));
|
||||
}
|
||||
|
||||
.fsp-title {
|
||||
margin: 0;
|
||||
font-size: 46px;
|
||||
font-weight: 700;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.fsp-artist {
|
||||
margin: 0;
|
||||
font-size: 25px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.fsp-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 17px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.fsp-meta-dot {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.fsp-stars {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.fsp-star-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
line-height: 0;
|
||||
transition: transform 80ms ease;
|
||||
}
|
||||
|
||||
.fsp-star-btn:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.fsp-next {
|
||||
margin: 3px 0 0;
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* ── Controls row: transport · time · actions ── */
|
||||
.fsp-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
/* Own semi-transparent bar, a bit darker than the info bar above for contrast.
|
||||
Full-width: cancel the foot's side padding like .fsp-info-row. Negative top
|
||||
margin cancels the foot's flex gap so this bar touches the info bar above
|
||||
(the gap to the waveform below stays). */
|
||||
margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-6)) 0;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.fsp-transport {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.fsp-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.fsp-time {
|
||||
justify-self: center;
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fsp-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.fsp-btn-sm {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.fsp-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fsp-btn.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Bottom-right action cluster: larger tap targets than the transport's small buttons. */
|
||||
.fsp-actions .fsp-btn-sm {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
/* Reused FsPlayBtn (renders .fs-btn.fs-btn-play) — scope its look to this player. */
|
||||
.fsp .playback-transport-play-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Match the plain transport buttons — no white circle. */
|
||||
.fsp .fs-btn-play {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.fsp .fs-btn-play:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── True waveform seekbar (WaveformSeek) — full-width media-center bar ──
|
||||
Replaces the old thin FsSeekbar. Time stays centered in the control row
|
||||
(FsTimeReadout). The taller canvas makes the foot grow upward, shifting the
|
||||
info/control rows up. */
|
||||
.fsp .waveform-seek-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* WaveformSeek hardcodes a 24px canvas inline for the player bar; override it
|
||||
here for the fullscreen view. Its ResizeObserver redraws at the new size. */
|
||||
.fsp .waveform-seek-container canvas {
|
||||
height: clamp(32px, 5vh, 52px) !important;
|
||||
}
|
||||
|
||||
/* ── "Up next" overlay (FsQueueModal) — semi-transparent popover anchored
|
||||
bottom-right at the queue button, no blur. Transparent backdrop just catches
|
||||
the outside click to close. ── */
|
||||
.fsq-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.fsq-panel {
|
||||
position: absolute;
|
||||
right: var(--space-6);
|
||||
bottom: 163px; /* flush with the top of the black info frame */
|
||||
width: min(420px, 40%);
|
||||
max-height: 62vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(18, 18, 22, 0.82);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fsq-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.fsq-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fsq-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fsq-close:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fsq-list {
|
||||
overflow-y: auto;
|
||||
padding: var(--space-2) 0;
|
||||
}
|
||||
|
||||
.fsq-empty {
|
||||
padding: var(--space-5);
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fsq-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-5);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fsq-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.fsq-item-pos {
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.fsq-item-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.fsq-item-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fsq-item-artist {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.fsq-item-dur {
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* ── Scrolling lyrics overlay (reuses FsLyricsApple) — semi-transparent, no blur.
|
||||
FsLyricsApple's .fsa-lyrics-container is position:absolute inset:0, so it fills
|
||||
this box. ── */
|
||||
.fsp-lyrics-overlay {
|
||||
position: absolute;
|
||||
top: var(--space-6);
|
||||
left: 14%;
|
||||
right: 14%;
|
||||
bottom: 360px; /* ends above the song-info / cover block; tune */
|
||||
z-index: 4;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Centered lyrics text in the fullscreen overlay (FsLyricsApple defaults to
|
||||
left-aligned for the old layout). */
|
||||
.fsp-lyrics-overlay .fsa-lyrics-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fsp-lyrics-overlay .fsa-lyric-line {
|
||||
text-align: center;
|
||||
transform-origin: center center;
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
@import './login-page.css';
|
||||
@import './loading-empty.css';
|
||||
@import './fullscreen-player-adaptive-portrait.css';
|
||||
@import './fullscreen-player-static.css';
|
||||
@import './context-menu.css';
|
||||
@import './css-tooltips.css';
|
||||
@import './playlists-page.css';
|
||||
|
||||
Reference in New Issue
Block a user