mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
Merge branch 'main' into feat/waveform-loudness-cache
This commit is contained in:
@@ -1,12 +1,24 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { getCachedUrl } from '../utils/imageCache';
|
||||
import { getCachedBlob } from '../utils/imageCache';
|
||||
|
||||
interface CachedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
src: string;
|
||||
cacheKey: string;
|
||||
}
|
||||
|
||||
// Delay between the consumer dropping a cached URL and the actual revoke,
|
||||
// giving the DOM <img> time to finish its decode of the URL we just took
|
||||
// away. 500 ms is comfortably above any realistic decode latency without
|
||||
// being long enough to leak meaningful memory.
|
||||
const URL_REVOKE_DELAY_MS = 500;
|
||||
|
||||
/**
|
||||
* Returns an object URL for a cached image. Each call owns its own URL: it
|
||||
* is created when the blob arrives and revoked (after a small grace delay)
|
||||
* on cleanup. There is no shared URL pool — the previous global LRU caused
|
||||
* "Failed to load resource" errors when an in-use URL got revoked because a
|
||||
* different consumer pushed it out of the cache.
|
||||
*
|
||||
* @param fallbackToFetch If true (default), returns the raw fetchUrl while the
|
||||
* blob is still resolving — useful for <img> tags so the browser starts
|
||||
* loading immediately. Pass false for CSS background-image consumers that
|
||||
@@ -17,11 +29,22 @@ export function useCachedUrl(fetchUrl: string, cacheKey: string, fallbackToFetch
|
||||
useEffect(() => {
|
||||
if (!fetchUrl) { setResolved(''); return; }
|
||||
const controller = new AbortController();
|
||||
let createdUrl: string | null = null;
|
||||
setResolved('');
|
||||
getCachedUrl(fetchUrl, cacheKey, controller.signal).then(url => {
|
||||
if (!controller.signal.aborted) setResolved(url);
|
||||
getCachedBlob(fetchUrl, cacheKey, controller.signal).then(blob => {
|
||||
if (controller.signal.aborted) return;
|
||||
if (blob) {
|
||||
createdUrl = URL.createObjectURL(blob);
|
||||
setResolved(createdUrl);
|
||||
}
|
||||
});
|
||||
return () => { controller.abort(); };
|
||||
return () => {
|
||||
controller.abort();
|
||||
if (createdUrl) {
|
||||
const url = createdUrl;
|
||||
setTimeout(() => URL.revokeObjectURL(url), URL_REVOKE_DELAY_MS);
|
||||
}
|
||||
};
|
||||
}, [fetchUrl, cacheKey]);
|
||||
return fallbackToFetch ? (resolved || fetchUrl) : resolved;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { buildCoverArtUrl, coverArtCacheKey, getArtistInfo, star, unstar } from '../api/subsonic';
|
||||
import { useCachedUrl } from './CachedImage';
|
||||
import { getCachedUrl } from '../utils/imageCache';
|
||||
import { getCachedBlob } from '../utils/imageCache';
|
||||
import { extractCoverColors } from '../utils/dynamicColors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLyrics, type WordLyricsLine } from '../hooks/useLyrics';
|
||||
@@ -740,7 +740,7 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) {
|
||||
if (!nextCoverArt) return;
|
||||
const url = buildCoverArtUrl(nextCoverArt, 300);
|
||||
const key = coverArtCacheKey(nextCoverArt, 300);
|
||||
getCachedUrl(url, key).catch(() => {});
|
||||
getCachedBlob(url, key).catch(() => {});
|
||||
}, [nextCoverArt]);
|
||||
|
||||
// Lyrics settings popover state
|
||||
|
||||
@@ -298,6 +298,12 @@ function QueuePanelHostOrSolo() {
|
||||
const enqueueAt = usePlayerStore(s => s.enqueueAt);
|
||||
const contextMenu = usePlayerStore(s => s.contextMenu);
|
||||
|
||||
// When the user picks a track *from* the queue list, suppress the
|
||||
// upcoming auto-scroll so their click target stays in view instead of
|
||||
// the list rebasing onto the next track. Auto-advance (natural playback)
|
||||
// never sets this flag, so it keeps its original "show what's next" behavior.
|
||||
const suppressNextAutoScrollRef = useRef(false);
|
||||
|
||||
const playbackSource = usePlayerStore(s => s.currentPlaybackSource);
|
||||
const normalizationNowDb = usePlayerStore(s => s.normalizationNowDb);
|
||||
const normalizationTargetLufs = usePlayerStore(s => s.normalizationTargetLufs);
|
||||
@@ -478,6 +484,10 @@ function QueuePanelHostOrSolo() {
|
||||
}, [enqueueAt]);
|
||||
|
||||
useEffect(function queueAutoScroll() {
|
||||
if (suppressNextAutoScrollRef.current) {
|
||||
suppressNextAutoScrollRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (!queueListRef.current || queueIndex < 0) return;
|
||||
if (activeTab !== 'queue') return;
|
||||
const songs = queueListRef.current!.querySelectorAll<HTMLElement>('[data-queue-idx]');
|
||||
@@ -916,7 +926,10 @@ function QueuePanelHostOrSolo() {
|
||||
<div
|
||||
data-queue-idx={idx}
|
||||
className={`queue-item ${isPlaying ? 'active' : ''} ${contextMenu.isOpen && contextMenu.type === 'queue-item' && contextMenu.queueIndex === idx ? 'context-active' : ''}`}
|
||||
onClick={() => playTrack(track, queue)}
|
||||
onClick={() => {
|
||||
suppressNextAutoScrollRef.current = true;
|
||||
playTrack(track, queue);
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
usePlayerStore.getState().openContextMenu(e.clientX, e.clientY, track, 'queue-item', idx);
|
||||
|
||||
Reference in New Issue
Block a user