feat: Update cover art retrieval to use higher resolution images and enhance download manager with new features
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user