fix: Playback stability, seek stop bug, and UI polish

- playerStore: Guard onend against spurious 'ended' events fired by
  WebKit/GStreamer immediately after a direct audioNode.currentTime seek.
  Root cause of the deterministic "second click on progress bar stops song"
  bug: a lastSeekAt timestamp + position check now silently drops any
  'ended' event that fires within 1 s of a seek if the playhead isn't
  actually near the track end.

- playerStore: Debounce togglePlay with a 300 ms lock to prevent rapid
  double-clicks from issuing pause→play before GStreamer has finished its
  state transition, which caused a multi-second pipeline hang.

- NowPlayingDropdown: Clicking a Live entry now navigates to the album page.

- QueuePanel: DnD drop target index now calculated from clientY position
  at drop time instead of refs, eliminating the dragend-before-drop race
  on macOS WKWebView and Windows WebView2.

- styles: Increased Hero background blur for a more immersive look.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-14 14:41:02 +01:00
parent 1e599d9636
commit baa701dd74
4 changed files with 359 additions and 332 deletions
+7 -1
View File
@@ -4,9 +4,11 @@ import { getNowPlaying, SubsonicNowPlaying, buildCoverArtUrl } from '../api/subs
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
export default function NowPlayingDropdown() {
const { t } = useTranslation();
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const [nowPlaying, setNowPlaying] = useState<SubsonicNowPlaying[]>([]);
const isPlaying = usePlayerStore(s => s.isPlaying);
@@ -121,7 +123,11 @@ export default function NowPlayingDropdown() {
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{visible.map((stream, idx) => (
<div key={`${stream.id}-${idx}`} style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', background: 'var(--bg-hover)', padding: '0.5rem', borderRadius: '8px' }}>
<div
key={`${stream.id}-${idx}`}
onClick={() => { if (stream.albumId) { setIsOpen(false); navigate(`/album/${stream.albumId}`); } }}
style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', background: 'var(--bg-hover)', padding: '0.5rem', borderRadius: '8px', cursor: stream.albumId ? 'pointer' : 'default' }}
>
<div style={{ width: '48px', height: '48px', flexShrink: 0, borderRadius: '6px', overflow: 'hidden', background: 'var(--bg-surface)' }}>
{stream.coverArt ? (
<img src={buildCoverArtUrl(stream.coverArt, 100)} alt="Cover" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />