feat: enhance mobile audio player with initialization and styling improvements

This commit is contained in:
2025-07-23 04:05:55 +00:00
committed by GitHub
parent a6e65888ab
commit 463be90779
3 changed files with 130 additions and 27 deletions

View File

@@ -1 +1 @@
NEXT_PUBLIC_COMMIT_SHA=437640c NEXT_PUBLIC_COMMIT_SHA=a6e6588

View File

@@ -94,6 +94,42 @@ export const AudioPlayer: React.FC = () => {
} }
} }
// Mobile-specific audio initialization
if (isMobile) {
// Add a document click listener to initialize audio context on first user interaction
const initializeAudioOnMobile = async () => {
if (!audioInitialized) {
try {
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) {
const audioContext = new AudioContextClass();
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
setAudioInitialized(true);
}
} catch (error) {
console.log('Mobile audio context initialization failed:', error);
}
}
};
// Listen for any user interaction to initialize audio
const handleFirstUserInteraction = () => {
initializeAudioOnMobile();
document.removeEventListener('touchstart', handleFirstUserInteraction);
document.removeEventListener('click', handleFirstUserInteraction);
};
document.addEventListener('touchstart', handleFirstUserInteraction, { passive: true });
document.addEventListener('click', handleFirstUserInteraction);
return () => {
document.removeEventListener('touchstart', handleFirstUserInteraction);
document.removeEventListener('click', handleFirstUserInteraction);
};
}
// Clean up old localStorage entries with track IDs // Clean up old localStorage entries with track IDs
const keysToRemove: string[] = []; const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
@@ -103,7 +139,7 @@ export const AudioPlayer: React.FC = () => {
} }
} }
keysToRemove.forEach(key => localStorage.removeItem(key)); keysToRemove.forEach(key => localStorage.removeItem(key));
}, []); }, [isMobile, audioInitialized]);
// Apply volume to audio element when volume changes // Apply volume to audio element when volume changes
useEffect(() => { useEffect(() => {
@@ -364,47 +400,73 @@ export const AudioPlayer: React.FC = () => {
const togglePlayPause = async () => { const togglePlayPause = async () => {
if (audioCurrent && currentTrack) { if (audioCurrent && currentTrack) {
// On mobile, ensure audio is initialized on first user interaction
if (isMobile && !audioInitialized) {
try {
// Create a dummy audio context to initialize audio on mobile
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) {
const audioContext = new AudioContextClass();
await audioContext.resume();
setAudioInitialized(true);
}
} catch (error) {
console.log('Audio context initialization failed:', error);
}
}
if (isPlaying) { if (isPlaying) {
audioCurrent.pause(); audioCurrent.pause();
setIsPlaying(false); setIsPlaying(false);
onTrackPause(audioCurrent.currentTime); onTrackPause(audioCurrent.currentTime);
} else { } else {
try { try {
// On mobile, ensure audio element is properly loaded before playing
if (isMobile) {
// Ensure the audio element has the correct source
if (audioCurrent.src !== currentTrack.url) {
audioCurrent.src = currentTrack.url;
audioCurrent.load(); // Force reload the audio element
}
// Wait for the audio to be ready to play
if (audioCurrent.readyState < 3) { // HAVE_FUTURE_DATA
await new Promise((resolve, reject) => {
const handleCanPlay = () => {
audioCurrent.removeEventListener('canplay', handleCanPlay);
audioCurrent.removeEventListener('error', handleError);
resolve(void 0);
};
const handleError = () => {
audioCurrent.removeEventListener('canplay', handleCanPlay);
audioCurrent.removeEventListener('error', handleError);
reject(new Error('Audio failed to load'));
};
audioCurrent.addEventListener('canplay', handleCanPlay);
audioCurrent.addEventListener('error', handleError);
});
}
}
await audioCurrent.play(); await audioCurrent.play();
setIsPlaying(true); setIsPlaying(true);
setAudioInitialized(true);
onTrackPlay(currentTrack); onTrackPlay(currentTrack);
} catch (error) { } catch (error) {
console.error('Failed to play audio:', error); console.error('Failed to play audio:', error);
// Try to initialize audio context and retry
// Additional mobile-specific handling
if (isMobile) { if (isMobile) {
try { try {
// Try creating and resuming audio context
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) { if (AudioContextClass) {
const audioContext = new AudioContextClass(); const audioContext = new AudioContextClass();
await audioContext.resume(); if (audioContext.state === 'suspended') {
await audioContext.resume();
}
setAudioInitialized(true); setAudioInitialized(true);
await audioCurrent.play();
setIsPlaying(true);
onTrackPlay(currentTrack);
} }
// Retry playing
await audioCurrent.play();
setIsPlaying(true);
onTrackPlay(currentTrack);
} catch (retryError) { } catch (retryError) {
console.error('Audio play retry failed:', retryError); console.error('Audio play retry failed:', retryError);
setIsPlaying(false); setIsPlaying(false);
// Show user-friendly error on mobile
toast({
variant: "destructive",
title: "Playback Error",
description: "Unable to play audio. Please try again or check your connection.",
});
} }
} else { } else {
setIsPlaying(false); setIsPlaying(false);
@@ -468,11 +530,13 @@ export const AudioPlayer: React.FC = () => {
{/* Mobile controls */} {/* Mobile controls */}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95" className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
toggleCurrentTrackStar(); toggleCurrentTrackStar();
}} }}
type="button"
aria-label={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'} title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
> >
<Heart <Heart
@@ -480,20 +544,30 @@ export const AudioPlayer: React.FC = () => {
/> />
</button> </button>
<button <button
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95" className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
onClick={playPreviousTrack} onClick={playPreviousTrack}
type="button"
aria-label="Previous track"
> >
<FaBackward className="w-4 h-4" /> <FaBackward className="w-4 h-4" />
</button> </button>
<button <button
className="p-4 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 bg-primary/10" className="p-4 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 bg-primary/10 touch-manipulation"
onClick={togglePlayPause} onClick={togglePlayPause}
onTouchStart={(e) => {
// Prevent iOS double-tap zoom on the play button
e.preventDefault();
}}
type="button"
aria-label={isPlaying ? 'Pause' : 'Play'}
> >
{isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />} {isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />}
</button> </button>
<button <button
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95" className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
onClick={playNextTrack} onClick={playNextTrack}
type="button"
aria-label="Next track"
> >
<FaForward className="w-4 h-4" /> <FaForward className="w-4 h-4" />
</button> </button>
@@ -688,9 +762,10 @@ export const AudioPlayer: React.FC = () => {
ref={audioRef} ref={audioRef}
hidden hidden
playsInline playsInline
preload="auto" preload={isMobile ? "none" : "auto"}
controls={false} controls={false}
crossOrigin="anonymous" crossOrigin="anonymous"
webkit-playsinline="true"
/> />
<audio ref={preloadAudioRef} hidden preload="metadata" /> <audio ref={preloadAudioRef} hidden preload="metadata" />
</> </>

View File

@@ -880,6 +880,34 @@
bottom: calc(4rem + env(safe-area-inset-bottom, 0)); bottom: calc(4rem + env(safe-area-inset-bottom, 0));
} }
/* Mobile Audio Player Styles */
.mobile-audio-player {
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mobile-audio-player button {
-webkit-tap-highlight-color: transparent;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Prevent iOS zoom on input focus */
@media screen and (max-width: 767px) {
input[type="range"] {
font-size: 16px;
}
/* Improve button touch targets */
.mobile-audio-player button {
min-height: 44px;
min-width: 44px;
}
}
/* Better focus states for accessibility */ /* Better focus states for accessibility */
button:focus-visible { button:focus-visible {