feat: redesign backing track player UI to match design

Redesigned UI to match cleaner, more minimal design from mockups:

Visual changes:
- Circular icon buttons (play and stop) instead of rectangular
- Removed volume control slider (not in design)
- Cleaner title formatting ("Backing Track: filename")
- Improved seek bar with progress gradient
- Monospace font for time display
- Simplified loop checkbox styling
- Clean "Close" button styling

Technical changes:
- Removed unused volume state and handlers
- Removed unused imports (Button, volume icons, mixers selector)
- Consistent styling between popup and modal versions
- Updated button states (play = blue, pause = gray, stop = gray)
- Better spacing and layout

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-14 19:47:26 +05:30
parent abf6ba7105
commit 197d91e9af
1 changed files with 240 additions and 200 deletions

View File

@ -1,10 +1,8 @@
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button, FormGroup, Label, Input } from 'reactstrap';
import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlay, faPause, faStop, faVolumeUp } from '@fortawesome/free-solid-svg-icons';
import { faPlay, faPause, faStop } from '@fortawesome/free-solid-svg-icons';
import useMediaActions from '../../hooks/useMediaActions';
import { selectBackingTrackMixers } from '../../store/features/mixersSlice';
const JKSessionBackingTrackPlayer = ({
isOpen,
@ -16,11 +14,9 @@ const JKSessionBackingTrackPlayer = ({
isPopup = false
}) => {
const { closeMedia } = useMediaActions();
const backingTrackMixers = useSelector(selectBackingTrackMixers);
const [isPlaying, setIsPlaying] = useState(false);
const [isLooping, setIsLooping] = useState(false);
const [volume, setVolume] = useState(100);
const [currentTime, setCurrentTime] = useState('0:00');
const [duration, setDuration] = useState('0:00');
const [currentPositionMs, setCurrentPositionMs] = useState(0);
@ -37,16 +33,6 @@ const JKSessionBackingTrackPlayer = ({
// Visibility tracking for performance optimization
const [isVisible, setIsVisible] = useState(true);
const volumeRef = useRef(null);
const trackVolumeObjectRef = useRef({
volL: 0,
volR: 0,
pan: 0,
mute: false,
solo: false,
loop: false
});
// UAT-003 Investigation: Track seek attempts while paused
const pendingSeekPositionRef = useRef(null);
@ -358,62 +344,6 @@ const JKSessionBackingTrackPlayer = ({
}
}, [isOperating, jamClient, handleError, clearError]);
const handleVolumeChange = useCallback(async (e) => {
const newVolume = parseInt(e.target.value);
const previousVolume = volume;
setVolume(newVolume);
try {
if (!jamClient) {
handleError('general', 'Audio engine not available', null);
setVolume(previousVolume); // Revert to previous
return;
}
console.log('[VOL] Volume change requested:', newVolume, 'Mixers:', backingTrackMixers);
// Volume is controlled through the mixer system
// Get the backing track mixer (use personal mixer)
const backingTrackMixer = backingTrackMixers[0]?.mixers?.personal?.mixer;
console.log('[VOL] Backing track mixer:', backingTrackMixer);
if (!backingTrackMixer) {
console.warn('[VOL] No backing track mixer found, volume control unavailable');
// No mixer available, silently skip
return;
}
// Convert 0-100 to dB range (-80 to +20)
// Using FaderHelpers conversion formula
const minDb = -80;
const maxDb = 20;
const volumeDb = minDb + (newVolume / 100) * (maxDb - minDb);
console.log('[VOL] Setting volume to', volumeDb, 'dB');
// Update track volume object
trackVolumeObjectRef.current = {
...trackVolumeObjectRef.current,
volL: volumeDb,
volR: volumeDb
};
// Set volume through mixer system
await jamClient.SessionSetTrackVolumeData(
backingTrackMixer.id,
backingTrackMixer.mode,
trackVolumeObjectRef.current
);
console.log('[VOL] Volume set successfully');
} catch (error) {
console.error('[VOL] Volume change failed:', error);
handleError('playback', 'Failed to adjust volume', error);
setVolume(previousVolume); // Revert to previous on error
}
}, [volume, jamClient, backingTrackMixers, handleError]);
const handleLoopChange = useCallback(async (e) => {
const shouldLoop = e.target.checked;
const previousLoop = isLooping;
@ -568,19 +498,23 @@ const JKSessionBackingTrackPlayer = ({
{/* Popup Content */}
<div style={{
flex: 1,
padding: '20px',
padding: '30px 20px',
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{ maxWidth: '400px', width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* Row 1: Backing Track */}
<div style={{ display: 'flex', alignItems: 'center', padding: '12px', backgroundColor: '#e9ecef', borderRadius: '4px' }}>
<strong style={{ marginRight: '8px' }}>Backing Track:</strong>
<span>{getFileName(backingTrack)}</span>
<div style={{ maxWidth: '420px', width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Title */}
<div style={{
color: '#666',
fontSize: '14px',
fontWeight: 'normal'
}}>
<span style={{ color: '#999' }}>Backing Track: </span>
{getFileName(backingTrack)}
</div>
{/* Error Display */}
@ -616,79 +550,118 @@ const JKSessionBackingTrackPlayer = ({
</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"
{/* Controls Section */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* Circular Buttons and Seek Bar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Play Button - Circular */}
<button
onClick={handlePlay}
disabled={!backingTrack || isLoadingDuration || isOperating || error}
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
border: 'none',
backgroundColor: isPlaying ? '#6c757d' : '#5b9bd5',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: (!backingTrack || isLoadingDuration || isOperating || error) ? 'not-allowed' : 'pointer',
opacity: (!backingTrack || isLoadingDuration || isOperating || error) ? 0.5 : 1,
outline: 'none',
flexShrink: 0
}}
>
<FontAwesomeIcon icon={isPlaying ? faPause : faPlay} />
{isOperating && ' ...'}
</Button>
<FontAwesomeIcon icon={isPlaying ? faPause : faPlay} style={{ fontSize: '14px' }} />
</button>
<Button
color="secondary"
{/* Stop Button - Circular */}
<button
onClick={handleStop}
disabled={!backingTrack || isLoadingDuration || isOperating}
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
border: 'none',
backgroundColor: '#b0b0b0',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: (!backingTrack || isLoadingDuration || isOperating) ? 'not-allowed' : 'pointer',
opacity: (!backingTrack || isLoadingDuration || isOperating) ? 0.5 : 1,
outline: 'none',
flexShrink: 0
}}
>
<FontAwesomeIcon icon={faStop} />
</Button>
<FontAwesomeIcon icon={faStop} style={{ fontSize: '12px' }} />
</button>
{/* Time and Seek Bar */}
<div style={{ display: 'flex', alignItems: 'center', flex: 1, gap: '8px' }}>
<span style={{ fontSize: '14px', color: '#666', minWidth: '35px', fontFamily: 'monospace' }}>{currentTime}</span>
<input
type="range"
min="0"
max={durationMs || 100}
value={currentPositionMs}
onChange={handleSeek}
disabled={!backingTrack || isLoadingDuration}
style={{
flex: 1,
height: '4px',
borderRadius: '2px',
outline: 'none',
background: `linear-gradient(to right, #5b9bd5 0%, #5b9bd5 ${(currentPositionMs / (durationMs || 1)) * 100}%, #ddd ${(currentPositionMs / (durationMs || 1)) * 100}%, #ddd 100%)`,
WebkitAppearance: 'none',
cursor: (!backingTrack || isLoadingDuration) ? 'not-allowed' : 'pointer'
}}
/>
<span style={{ fontSize: '14px', color: '#666', minWidth: '35px', fontFamily: 'monospace' }}>{duration}</span>
</div>
</div>
{/* Seek bar with duration */}
<div style={{ display: 'flex', alignItems: 'center', width: '100%', gap: '8px' }}>
<span style={{ fontSize: '14px', minWidth: '40px' }}>{currentTime}</span>
<input
type="range"
min="0"
max={durationMs || 100}
value={currentPositionMs}
onChange={handleSeek}
style={{ flex: 1 }}
disabled={!backingTrack || isLoadingDuration}
/>
<span style={{ fontSize: '14px', minWidth: '40px' }}>{duration}</span>
{/* Loop Checkbox */}
<div style={{ display: 'flex', justifyContent: 'flex-start', paddingLeft: '0px' }}>
<label style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
fontSize: '14px',
color: '#666',
userSelect: 'none'
}}>
<input
type="checkbox"
checked={isLooping}
onChange={handleLoopChange}
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
Loop playback
</label>
</div>
</div>
{/* Row 3: Loop Checkbox */}
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '8px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type="checkbox"
checked={isLooping}
onChange={handleLoopChange}
style={{ marginRight: '8px' }}
/>
Loop playback
</label>
</div>
{/* Row 4: Volume Control */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '8px' }}>
<FontAwesomeIcon icon={faVolumeUp} />
<input
ref={volumeRef}
type="range"
min="0"
max="100"
value={volume}
onChange={handleVolumeChange}
style={{ width: '200px' }}
/>
<span style={{ fontWeight: 'bold', minWidth: '40px' }}>{volume}%</span>
</div>
{/* Close Button */}
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '16px' }}>
<Button
color="secondary"
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '8px' }}>
<button
onClick={handleClose}
style={{
padding: '8px 24px',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: 'white',
color: '#666',
cursor: 'pointer',
fontSize: '14px',
outline: 'none'
}}
>
Close
</Button>
</button>
</div>
</div>
</div>
@ -705,11 +678,15 @@ const JKSessionBackingTrackPlayer = ({
</ModalHeader>
<ModalBody>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* Row 1: Backing Track */}
<div style={{ display: 'flex', alignItems: 'center', padding: '8px', backgroundColor: '#f8f9fa', borderRadius: '4px' }}>
<strong style={{ marginRight: '8px' }}>Backing Track:</strong>
<span>{getFileName(backingTrack)}</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Title */}
<div style={{
color: '#666',
fontSize: '14px',
fontWeight: 'normal'
}}>
<span style={{ color: '#999' }}>Backing Track: </span>
{getFileName(backingTrack)}
</div>
{/* Error Display */}
@ -726,13 +703,34 @@ const JKSessionBackingTrackPlayer = ({
}}>
<div>{error}</div>
<div style={{ display: 'flex', gap: '8px' }}>
<Button size="sm" color="secondary" onClick={clearError}>
<button
onClick={clearError}
style={{
padding: '4px 12px',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: 'white',
cursor: 'pointer',
fontSize: '12px'
}}
>
Dismiss
</Button>
</button>
{(errorType === 'network' || errorType === 'file') && (
<Button size="sm" color="primary" onClick={retryOperation}>
<button
onClick={retryOperation}
style={{
padding: '4px 12px',
border: '1px solid #5b9bd5',
borderRadius: '4px',
backgroundColor: '#5b9bd5',
color: 'white',
cursor: 'pointer',
fontSize: '12px'
}}
>
Retry
</Button>
</button>
)}
</div>
</div>
@ -745,77 +743,119 @@ const JKSessionBackingTrackPlayer = ({
</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"
{/* Controls Section */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* Circular Buttons and Seek Bar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Play Button - Circular */}
<button
onClick={handlePlay}
disabled={!backingTrack || isLoadingDuration || isOperating || error}
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
border: 'none',
backgroundColor: isPlaying ? '#6c757d' : '#5b9bd5',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: (!backingTrack || isLoadingDuration || isOperating || error) ? 'not-allowed' : 'pointer',
opacity: (!backingTrack || isLoadingDuration || isOperating || error) ? 0.5 : 1,
outline: 'none',
flexShrink: 0
}}
>
<FontAwesomeIcon icon={isPlaying ? faPause : faPlay} /> Play/Pause
{isOperating && ' ...'}
</Button>
<FontAwesomeIcon icon={isPlaying ? faPause : faPlay} style={{ fontSize: '14px' }} />
</button>
<Button
color="secondary"
{/* Stop Button - Circular */}
<button
onClick={handleStop}
disabled={!backingTrack || isLoadingDuration || isOperating}
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
border: 'none',
backgroundColor: '#b0b0b0',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: (!backingTrack || isLoadingDuration || isOperating) ? 'not-allowed' : 'pointer',
opacity: (!backingTrack || isLoadingDuration || isOperating) ? 0.5 : 1,
outline: 'none',
flexShrink: 0
}}
>
<FontAwesomeIcon icon={faStop} /> Stop
</Button>
<FontAwesomeIcon icon={faStop} style={{ fontSize: '12px' }} />
</button>
{/* Time and Seek Bar */}
<div style={{ display: 'flex', alignItems: 'center', flex: 1, gap: '8px' }}>
<span style={{ fontSize: '14px', color: '#666', minWidth: '35px', fontFamily: 'monospace' }}>{currentTime}</span>
<input
type="range"
min="0"
max={durationMs || 100}
value={currentPositionMs}
onChange={handleSeek}
disabled={!backingTrack || isLoadingDuration}
style={{
flex: 1,
height: '4px',
borderRadius: '2px',
outline: 'none',
background: `linear-gradient(to right, #5b9bd5 0%, #5b9bd5 ${(currentPositionMs / (durationMs || 1)) * 100}%, #ddd ${(currentPositionMs / (durationMs || 1)) * 100}%, #ddd 100%)`,
WebkitAppearance: 'none',
cursor: (!backingTrack || isLoadingDuration) ? 'not-allowed' : 'pointer'
}}
/>
<span style={{ fontSize: '14px', color: '#666', minWidth: '35px', fontFamily: 'monospace' }}>{duration}</span>
</div>
</div>
{/* Seek bar with duration */}
<div style={{ display: 'flex', alignItems: 'center', width: '100%', gap: '8px' }}>
<span style={{ fontSize: '14px', minWidth: '40px' }}>{currentTime}</span>
<input
type="range"
min="0"
max={durationMs || 100}
value={currentPositionMs}
onChange={handleSeek}
style={{ flex: 1 }}
disabled={!backingTrack || isLoadingDuration}
/>
<span style={{ fontSize: '14px', minWidth: '40px' }}>{duration}</span>
{/* Loop Checkbox */}
<div style={{ display: 'flex', justifyContent: 'flex-start', paddingLeft: '0px' }}>
<label style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
fontSize: '14px',
color: '#666',
userSelect: 'none'
}}>
<input
type="checkbox"
checked={isLooping}
onChange={handleLoopChange}
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
Loop playback
</label>
</div>
</div>
{/* Row 3: Loop Checkbox */}
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '8px' }}>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
<input
type="checkbox"
checked={isLooping}
onChange={handleLoopChange}
style={{ marginRight: '8px' }}
/>
Loop playback
</label>
</div>
{/* Row 4: Volume Control */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '8px' }}>
<FontAwesomeIcon icon={faVolumeUp} />
<input
ref={volumeRef}
type="range"
min="0"
max="100"
value={volume}
onChange={handleVolumeChange}
style={{ width: '200px' }}
/>
<span style={{ fontWeight: 'bold', minWidth: '40px' }}>{volume}%</span>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={onClose}>
<button
onClick={onClose}
style={{
padding: '8px 24px',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: 'white',
color: '#666',
cursor: 'pointer',
fontSize: '14px',
outline: 'none'
}}
>
Close
</Button>
</button>
</ModalFooter>
</Modal>
);