feat: enhance mobile audio player with initialization and styling improvements
This commit is contained in:
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_COMMIT_SHA=437640c
|
NEXT_PUBLIC_COMMIT_SHA=a6e6588
|
||||||
|
|||||||
@@ -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" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user