mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(playlists): suggestion-row preview UX (#365)
* feat(playlists): suggestion-row preview UX (30s preview, double-click play-next, scroll keep)
Reworks the interaction on the suggestion rows below a playlist so users
can audition songs before deciding what to do with them.
- New "Preview" pill button left of each track title plays a 30-second
mid-song sample via a parallel HTML5 audio element. The main player
pauses on the first preview and auto-resumes when the preview ends.
Switching previews chains without re-resuming. External main-player
playback (spacebar, mediakey) cancels the preview without resuming.
Animated underline shows the 30 s progress.
- Double-click the row to insert the song at queueIndex + 1 and skip to
it ("Play next"). Single-click is intentionally inert so a stray click
next to the preview button no longer drops a song into the queue by
accident. Tooltip on the row makes the affordance discoverable.
- The + button still adds to the playlist, and now restores the
.main-content scroll position so the page no longer jumps to the top
after pressing it.
- i18n keys in all 8 locales: playlists.preview / previewStop / previewShort
/ previewStopShort / suggestionDoubleClickPlayNext.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(playlists): WebKitGTK NULL-instance crash on preview teardown
cucadmuh hit GLib-GObject-CRITICAL: invalid (NULL) pointer instance /
g_signal_connect_data assertion failed, with the UI freezing after
clicking a preview button. Root cause was the audio-element lifecycle:
- `audio.src = ''` to "stop" leaves WebKitGTK's GStreamer playbin in a
half-initialized state. The next signal_connect dereferences NULL.
- Creating `new Audio()` per click stacked half-torn-down playbins
during rapid switches.
- `loadedmetadata` listeners on orphaned audio elements could call
play() on an already-discarded instance.
Fix: reuse one <audio> element per component, tear down the source
via removeAttribute('src') + load() (which resets the playbin
cleanly), and gate async listeners behind a session counter so stale
metadata events on switched-away previews are ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(playlists): icon-based action buttons + add-on-doubleclick + hint
- Replace text-pill Preview button with circular icon button + animated
SVG progress ring (Play/Square icon swap when active).
- New Play-next button (filled accent circle, white triangle) sits
left of the Preview button — explicit affordance for the action that
used to be a hidden double-click.
- Double-click on a suggestion row now triggers Add-to-playlist (the
same action as the + button on the right) — discoverable shortcut
for mouse users.
- Subtitle under the Suggested Songs header announces the add-on-
doubleclick affordance, in all 8 locales.
- Drop the now-obsolete previewShort / previewStopShort short-label
keys (text replaced by icons); rename suggestionDoubleClickPlayNext
→ playNextSuggestion to match its new role on the Play-next button.
- Keep the session-counter guard from the prior commit so async
loadedmetadata handlers from a switched-away preview can't play()
on a discarded element.
Note: header column labels visually drift from the data columns when
suggestion rows have the action buttons; left as-is for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(playlists): apply LUFS pre-analysis attenuation to suggestion previews
When the main player is on loudness normalization, analysed tracks come
out reduced toward target while the unanalysed preview <audio> blasts
the file at its natural level — cucadmuh reported the previews are
audibly louder than the playlist playback.
Apply the user's stored pre-analysis attenuation (the slider value, not
the target-offset effective form) as a linear gain on the preview's
audio.volume. Default −4.5 dB lands the preview at ~60% of the player
volume, which roughly tracks how aggressively the Rust engine pulls
naturally-loud tracks toward target.
If the engine is off, behavior is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(playlists): use chevron icon for suggestion preview button
Distinguishes the preview button from the adjacent play-next button
by mirroring the Play Next chevron from the context menu.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
70ff025ece
commit
2bea55bedd
@@ -1668,6 +1668,111 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Suggestion-row action buttons — sit left of the track title */
|
||||
.track-info-suggestion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.playlist-suggestion-play-btn {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
color: var(--ctp-crust);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.85;
|
||||
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.playlist-suggestion-play-btn:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.playlist-suggestion-play-icon {
|
||||
display: block;
|
||||
/* optical center for right-weighted triangle */
|
||||
transform: translateX(0.5px);
|
||||
}
|
||||
|
||||
.playlist-suggestion-preview-btn {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.playlist-suggestions .track-row:hover .playlist-suggestion-preview-btn,
|
||||
.playlist-suggestion-preview-btn:hover,
|
||||
.playlist-suggestion-preview-btn.is-previewing {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.playlist-suggestion-preview-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.playlist-suggestion-preview-ring-track {
|
||||
fill: none;
|
||||
stroke: var(--ctp-overlay0);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.playlist-suggestion-preview-ring-progress {
|
||||
fill: none;
|
||||
stroke: var(--accent);
|
||||
stroke-width: 1.5;
|
||||
/* circumference = 2π × 10.5 ≈ 65.97 */
|
||||
stroke-dasharray: 65.97;
|
||||
stroke-dashoffset: 65.97;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.playlist-suggestion-preview-icon {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Play triangle is right-weighted; nudge for optical center */
|
||||
.playlist-suggestion-preview-icon-play {
|
||||
transform: translateX(0.5px);
|
||||
}
|
||||
|
||||
.playlist-suggestion-preview-btn.is-previewing .playlist-suggestion-preview-ring-progress {
|
||||
animation: playlist-preview-progress 30s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes playlist-preview-progress {
|
||||
from { stroke-dashoffset: 65.97; }
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
.track-row:hover,
|
||||
.track-row.context-active {
|
||||
background: var(--bg-hover);
|
||||
@@ -7884,6 +7989,19 @@ html.no-compositing .fs-seekbar-played {
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.playlist-suggestions-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.playlist-suggestions-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* ─ Context Menu Submenu ─ */
|
||||
.context-menu-item--submenu {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user