feat(03-02): add loading states and disabled UI
Implemented loading states and disabled controls during operations: Loading states: - isLoadingDuration: Tracks duration fetch on track open - isOperating: Prevents rapid clicks on play/stop buttons Duration loading: - Shows "Loading track..." indicator while fetching - Disables all controls until ready - Sets/clears loading state in fetchDuration Operation flags: - handlePlay: Checks isOperating, uses finally block to reset - handleStop: Checks isOperating, uses finally block to reset - handleSeek: Allows immediate UI update (no blocking) Button disabled logic: - Play/Pause: disabled when (!backingTrack || isLoadingDuration || isOperating || error) - Stop: disabled when (!backingTrack || isLoadingDuration || isOperating) - Slider: disabled when (!backingTrack || isLoadingDuration) Loading indicators: - "Loading track..." text during duration fetch - "..." suffix on Play/Pause button during operations - Applied to both popup and modal versions Prevents rapid-click issues with isOperating guard.
This commit is contained in:
parent
acbddd35e3
commit
9c0455dc67
|
|
@ -30,6 +30,10 @@ const JKSessionBackingTrackPlayer = ({
|
|||
const [error, setError] = useState(null);
|
||||
const [errorType, setErrorType] = useState(null); // 'file', 'network', 'playback', 'general'
|
||||
|
||||
// Loading states
|
||||
const [isLoadingDuration, setIsLoadingDuration] = useState(true);
|
||||
const [isOperating, setIsOperating] = useState(false); // Operation in progress
|
||||
|
||||
const volumeRef = useRef(null);
|
||||
const trackVolumeObjectRef = useRef({
|
||||
volL: 0,
|
||||
|
|
@ -84,9 +88,11 @@ const JKSessionBackingTrackPlayer = ({
|
|||
|
||||
// Fetch and set duration immediately when track loads (async)
|
||||
const fetchDuration = async () => {
|
||||
setIsLoadingDuration(true);
|
||||
try {
|
||||
if (!jamClient) {
|
||||
handleError('general', 'Audio engine not available', null);
|
||||
setIsLoadingDuration(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -101,16 +107,19 @@ const JKSessionBackingTrackPlayer = ({
|
|||
handleError('file', 'Failed to load track duration. The file may be invalid or unsupported.', null);
|
||||
setDuration('0:00');
|
||||
setDurationMs(0);
|
||||
setIsLoadingDuration(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDurationMs(validDuration);
|
||||
setDuration(formatTime(validDuration));
|
||||
clearError(); // Clear any previous errors
|
||||
setIsLoadingDuration(false);
|
||||
} catch (error) {
|
||||
handleError('file', 'Failed to load track duration', error);
|
||||
setDuration('0:00');
|
||||
setDurationMs(0);
|
||||
setIsLoadingDuration(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -183,6 +192,9 @@ const JKSessionBackingTrackPlayer = ({
|
|||
}, [isPlaying, jamClient, backingTrack]);
|
||||
|
||||
const handlePlay = async () => {
|
||||
if (isOperating) return; // Prevent rapid clicks
|
||||
setIsOperating(true);
|
||||
|
||||
try {
|
||||
if (!jamClient) {
|
||||
handleError('general', 'Audio engine not available', null);
|
||||
|
|
@ -248,10 +260,15 @@ const JKSessionBackingTrackPlayer = ({
|
|||
handleError('playback', 'Failed to start playback', error);
|
||||
// Reset isPlaying to ensure UI is consistent
|
||||
setIsPlaying(false);
|
||||
} finally {
|
||||
setIsOperating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = async () => {
|
||||
if (isOperating) return; // Prevent rapid clicks
|
||||
setIsOperating(true);
|
||||
|
||||
try {
|
||||
if (!jamClient) {
|
||||
handleError('general', 'Audio engine not available', null);
|
||||
|
|
@ -273,6 +290,8 @@ const JKSessionBackingTrackPlayer = ({
|
|||
setCurrentTime('0:00');
|
||||
setCurrentPositionMs(0);
|
||||
pendingSeekPositionRef.current = null;
|
||||
} finally {
|
||||
setIsOperating(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -371,6 +390,10 @@ const JKSessionBackingTrackPlayer = ({
|
|||
const seekPositionMs = parseInt(e.target.value);
|
||||
const previousPosition = currentPositionMs;
|
||||
|
||||
// Update local state immediately for responsive UI (don't block on isOperating)
|
||||
setCurrentPositionMs(seekPositionMs);
|
||||
setCurrentTime(formatTime(seekPositionMs));
|
||||
|
||||
try {
|
||||
if (!jamClient) {
|
||||
handleError('general', 'Audio engine not available', null);
|
||||
|
|
@ -379,10 +402,6 @@ const JKSessionBackingTrackPlayer = ({
|
|||
|
||||
console.log('[BTP] handleSeek:', { seekPositionMs, isPlaying });
|
||||
|
||||
// Update local state immediately for responsive UI
|
||||
setCurrentPositionMs(seekPositionMs);
|
||||
setCurrentTime(formatTime(seekPositionMs));
|
||||
|
||||
// Seek the native client to the new position
|
||||
await jamClient.SessionTrackSeekMs(seekPositionMs);
|
||||
|
||||
|
|
@ -516,21 +535,29 @@ const JKSessionBackingTrackPlayer = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isLoadingDuration && (
|
||||
<div style={{ textAlign: 'center', color: '#6c757d', fontSize: '14px' }}>
|
||||
Loading track...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Controls */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }}>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handlePlay}
|
||||
disabled={!backingTrack}
|
||||
disabled={!backingTrack || isLoadingDuration || isOperating || error}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPlaying ? faPause : faPlay} />
|
||||
{isOperating && ' ...'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={handleStop}
|
||||
disabled={!backingTrack}
|
||||
disabled={!backingTrack || isLoadingDuration || isOperating}
|
||||
>
|
||||
<FontAwesomeIcon icon={faStop} />
|
||||
</Button>
|
||||
|
|
@ -546,7 +573,7 @@ const JKSessionBackingTrackPlayer = ({
|
|||
value={currentPositionMs}
|
||||
onChange={handleSeek}
|
||||
style={{ flex: 1 }}
|
||||
disabled={!backingTrack}
|
||||
disabled={!backingTrack || isLoadingDuration}
|
||||
/>
|
||||
<span style={{ fontSize: '14px', minWidth: '40px' }}>{duration}</span>
|
||||
</div>
|
||||
|
|
@ -637,21 +664,29 @@ const JKSessionBackingTrackPlayer = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isLoadingDuration && (
|
||||
<div style={{ textAlign: 'center', color: '#6c757d', fontSize: '14px' }}>
|
||||
Loading track...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Controls */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px' }}>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handlePlay}
|
||||
disabled={!backingTrack}
|
||||
disabled={!backingTrack || isLoadingDuration || isOperating || error}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPlaying ? faPause : faPlay} /> Play/Pause
|
||||
{isOperating && ' ...'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={handleStop}
|
||||
disabled={!backingTrack}
|
||||
disabled={!backingTrack || isLoadingDuration || isOperating}
|
||||
>
|
||||
<FontAwesomeIcon icon={faStop} /> Stop
|
||||
</Button>
|
||||
|
|
@ -667,7 +702,7 @@ const JKSessionBackingTrackPlayer = ({
|
|||
value={currentPositionMs}
|
||||
onChange={handleSeek}
|
||||
style={{ flex: 1 }}
|
||||
disabled={!backingTrack}
|
||||
disabled={!backingTrack || isLoadingDuration}
|
||||
/>
|
||||
<span style={{ fontSize: '14px', minWidth: '40px' }}>{duration}</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue