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:
Nuwan 2026-01-14 15:54:33 +05:30
parent acbddd35e3
commit 9c0455dc67
1 changed files with 45 additions and 10 deletions

View File

@ -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>