From 9c0455dc674f628ed3fb69037f7ed062caf0d8db Mon Sep 17 00:00:00 2001 From: Nuwan Date: Wed, 14 Jan 2026 15:54:33 +0530 Subject: [PATCH] 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. --- .../client/JKSessionBackingTrackPlayer.js | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js b/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js index b8c6c66a7..d9972a503 100644 --- a/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js +++ b/jam-ui/src/components/client/JKSessionBackingTrackPlayer.js @@ -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 = ({ )} + {/* Loading indicator */} + {isLoadingDuration && ( +
+ Loading track... +
+ )} + {/* Row 2: Controls */}
@@ -546,7 +573,7 @@ const JKSessionBackingTrackPlayer = ({ value={currentPositionMs} onChange={handleSeek} style={{ flex: 1 }} - disabled={!backingTrack} + disabled={!backingTrack || isLoadingDuration} /> {duration}
@@ -637,21 +664,29 @@ const JKSessionBackingTrackPlayer = ({
)} + {/* Loading indicator */} + {isLoadingDuration && ( +
+ Loading track... +
+ )} + {/* Row 2: Controls */}
@@ -667,7 +702,7 @@ const JKSessionBackingTrackPlayer = ({ value={currentPositionMs} onChange={handleSeek} style={{ flex: 1 }} - disabled={!backingTrack} + disabled={!backingTrack || isLoadingDuration} /> {duration}