feat: Update cover art retrieval to use higher resolution images and enhance download manager with new features

This commit is contained in:
2025-08-10 02:06:39 +00:00
committed by GitHub
parent 4b0997c6b4
commit 192148adf2
8 changed files with 189 additions and 46 deletions

View File

@@ -64,6 +64,22 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
const [activeTab, setActiveTab] = useState<MobileTab>('player');
const lyricsRef = useRef<HTMLDivElement>(null);
// Initialize volume from saved preference when fullscreen opens
useEffect(() => {
if (!isOpen) return;
try {
const savedVolume = localStorage.getItem('navidrome-volume');
if (savedVolume !== null) {
const vol = parseFloat(savedVolume);
if (!isNaN(vol) && vol >= 0 && vol <= 1) {
setVolume(vol);
const mainAudio = document.querySelector('audio') as HTMLAudioElement | null;
if (mainAudio) mainAudio.volume = vol;
}
}
} catch {}
}, [isOpen]);
// Debug logging for component changes
useEffect(() => {
console.log('🔍 FullScreenPlayer state changed:', {
@@ -128,7 +144,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
const containerHeight = scrollContainer.clientHeight;
const elementTop = currentLyricElement.offsetTop;
const elementHeight = currentLyricElement.offsetHeight;
const targetScrollTop = elementTop - (containerHeight / 2) + (elementHeight / 2);
// Position the active lyric higher on the screen (~25% from top)
const focusFraction = 0.25; // 0.5 would be center
const targetScrollTop = elementTop - (containerHeight * focusFraction) + (elementHeight / 2);
scrollContainer.scrollTo({
top: Math.max(0, targetScrollTop),
@@ -379,6 +397,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
mainAudio.currentTime = newTime;
setCurrentTime(newTime);
try {
localStorage.setItem('navidrome-current-track-time', newTime.toString());
} catch {}
};
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -388,6 +409,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
const newVolume = parseInt(e.target.value) / 100;
mainAudio.volume = newVolume;
setVolume(newVolume);
try {
localStorage.setItem('navidrome-volume', newVolume.toString());
} catch {}
};
const handleLyricClick = (time: number) => {
@@ -396,6 +420,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
mainAudio.currentTime = time;
setCurrentTime(time);
try {
localStorage.setItem('navidrome-current-track-time', time.toString());
} catch {}
// Update progress bar as well
if (duration > 0) {
@@ -660,18 +687,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
className="flex-1 overflow-y-auto"
ref={lyricsRef}
>
<div className="space-y-3 py-4">
<div className="space-y-4 py-10">
{lyrics.map((line, index) => (
<motion.div
key={index}
data-lyric-index={index}
onClick={() => handleLyricClick(line.time)}
initial={false}
animate={index === currentLyricIndex ? { scale: 1, opacity: 1 } : index < currentLyricIndex ? { scale: 0.995, opacity: 0.7 } : { scale: 0.99, opacity: 0.5 }}
animate={index === currentLyricIndex ? { scale: 1.06, opacity: 1 } : index < currentLyricIndex ? { scale: 0.985, opacity: 0.75 } : { scale: 0.98, opacity: 0.6 }}
transition={{ duration: 0.2 }}
className={`text-base leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground px-2 ${
className={`text-2xl sm:text-3xl leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground px-2 ${
index === currentLyricIndex
? 'text-foreground font-bold text-xl'
? 'text-foreground font-extrabold leading-tight text-5xl sm:text-6xl'
: index < currentLyricIndex
? 'text-foreground/60'
: 'text-foreground/40'
@@ -680,14 +707,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
wordWrap: 'break-word',
overflowWrap: 'break-word',
hyphens: 'auto',
paddingBottom: '4px'
paddingBottom: '4px',
// Subtle glow to make the current line feel elevated
textShadow: index === currentLyricIndex
? '0 4px 16px rgba(0,0,0,0.7), 0 0 24px rgba(255,255,255,0.16)'
: undefined
}}
title={`Click to jump to ${formatTime(line.time)}`}
>
{line.text || '♪'}
</motion.div>
))}
<div style={{ height: '200px' }} />
<div style={{ height: '260px' }} />
</div>
</div>
</motion.div>
@@ -727,8 +758,6 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
)}
</AnimatePresence>
</div>
{/* Mobile Tab Bar */}
<div className="flex-shrink-0 pb-safe">
<div className="flex justify-around py-4 mb-2">
<button
@@ -912,18 +941,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
>
<div className="h-full flex flex-col">
<ScrollArea className="flex-1 min-h-0">
<div className="space-y-3 pl-4 pr-4 py-4">
<div className="space-y-3 pl-4 pr-4 py-8">
{lyrics.map((line, index) => (
<motion.div
key={index}
data-lyric-index={index}
onClick={() => handleLyricClick(line.time)}
initial={false}
animate={index === currentLyricIndex ? { scale: 1, opacity: 1 } : index < currentLyricIndex ? { scale: 0.995, opacity: 0.75 } : { scale: 0.99, opacity: 0.5 }}
animate={index === currentLyricIndex ? { scale: 1.04, opacity: 1 } : index < currentLyricIndex ? { scale: 0.985, opacity: 0.75 } : { scale: 0.98, opacity: 0.5 }}
transition={{ duration: 0.2 }}
className={`text-base leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground ${
index === currentLyricIndex
? 'text-foreground font-bold text-2xl'
? 'text-foreground font-extrabold leading-tight text-5xl'
: index < currentLyricIndex
? 'text-foreground/60'
: 'text-foreground/40'
@@ -933,14 +962,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
overflowWrap: 'break-word',
hyphens: 'auto',
paddingBottom: '4px',
paddingLeft: '8px'
paddingLeft: '8px',
// Subtle glow to make the current line feel elevated
textShadow: index === currentLyricIndex
? '0 6px 18px rgba(0,0,0,0.7), 0 0 28px rgba(255,255,255,0.18)'
: undefined
}}
title={`Click to jump to ${formatTime(line.time)}`}
>
{line.text || '♪'}
</motion.div>
))}
<div style={{ height: '200px' }} />
<div style={{ height: '240px' }} />
</div>
</ScrollArea>
</div>

View File

@@ -109,7 +109,7 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
try {
const api = getNavidromeAPI();
const url = api ? api.getStreamUrl(song.id) : `offline-song-${song.id}`;
const coverArt = song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 64) : undefined;
const coverArt = song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 300) : undefined;
const track = {
id: song.id,
name: song.title,
@@ -140,7 +140,7 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
if (albumSongs.length > 0) {
const first = albumSongs[0];
const url = api ? api.getStreamUrl(first.id) : `offline-song-${first.id}`;
const coverArt = first.coverArt && api ? api.getCoverArtUrl(first.coverArt, 64) : undefined;
const coverArt = first.coverArt && api ? api.getCoverArtUrl(first.coverArt, 300) : undefined;
const track = {
id: first.id,
name: first.title,

View File

@@ -114,7 +114,7 @@ export default function SongsPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -135,7 +135,7 @@ export default function SongsPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred

View File

@@ -57,7 +57,7 @@ export default function PlaylistPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -77,7 +77,7 @@ export default function PlaylistPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -98,7 +98,7 @@ export default function PlaylistPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred

View File

@@ -199,7 +199,7 @@ export default function SearchPage() {
{/* Song Cover */}
<div className="shrink-0"> <Image
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 64) : '/default-user.jpg'}
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 300) : '/default-user.jpg'}
alt={song.album}
width={48}
height={48}