refactor(05-03): match backing track player layout with inline styles

Changes JamTrack player to use inline styles matching the backing track
player's simpler approach while keeping the macOS window chrome.

- Removed CSS file import, switched to inline styles
- Matched backing track player's control layout
- Circular play (36px) and stop (36px) buttons
- Time + scrubber + time in one row matching backing track
- Mix dropdown and "create custom mix" link below controls
- Close button at bottom centered
- All spacing and colors match backing track player

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-15 20:55:30 +05:30
parent 19b95fcce0
commit d870784a18
1 changed files with 396 additions and 171 deletions

View File

@ -10,7 +10,6 @@ import {
import { setOpenJamTrack, clearOpenJamTrack } from '../../store/features/sessionUISlice';
import { setAvailableMixdowns, setActiveMixdown } from '../../store/features/activeSessionSlice';
import { useJamServerContext } from '../../context/JamServerContext';
import './JKSessionJamTrackPlayer.css';
// Error types for comprehensive error handling
const ERROR_TYPES = {
@ -476,191 +475,417 @@ const JKSessionJamTrackPlayer = ({
if (!isOpen && !isPopup) return null;
return (
<div className="jamtrack-player-window">
<>
<style>
{`
@keyframes spin {
to { transform: rotate(360deg); }
}
`}
</style>
<div style={{
width: '420px',
minHeight: '200px',
display: 'flex',
flexDirection: 'column',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif',
backgroundColor: '#f5f5f7',
borderRadius: '8px',
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.3)'
}}>
{/* Window Chrome */}
<div className="jamtrack-player-chrome">
<div className="jamtrack-player-traffic-lights">
<div className="traffic-light red" onClick={onClose}></div>
<div className="traffic-light yellow"></div>
<div className="traffic-light green"></div>
<div style={{
display: 'flex',
alignItems: 'center',
padding: '12px 16px',
background: 'linear-gradient(180deg, #ffffff 0%, #f5f5f7 100%)',
borderBottom: '1px solid #d1d1d6',
borderRadius: '8px 8px 0 0'
}}>
<div style={{ display: 'flex', gap: '8px', marginRight: '12px' }}>
<div
onClick={onClose}
style={{
width: '12px',
height: '12px',
borderRadius: '50%',
border: '0.5px solid rgba(0, 0, 0, 0.1)',
background: '#ff5f56',
cursor: 'pointer'
}}
></div>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
border: '0.5px solid rgba(0, 0, 0, 0.1)',
background: '#ffbd2e'
}}></div>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
border: '0.5px solid rgba(0, 0, 0, 0.1)',
background: '#27c93f'
}}></div>
</div>
<div className="jamtrack-player-title">
<div style={{
flex: 1,
textAlign: 'center',
fontSize: '14px',
fontWeight: '500',
color: '#1d1d1f'
}}>
JamTrack: {jamTrack?.name || 'Loading...'}
</div>
</div>
{/* Content Area */}
<div className="jamtrack-player-content">
{/* Error Banner */}
{error && (
<div className={`jamtrack-error-banner ${(error.type === ERROR_TYPES.PLAYBACK || error.type === ERROR_TYPES.GENERAL) ? 'warning' : ''}`}>
<strong>{error.type.toUpperCase()} ERROR:</strong>
<div>{error.message}</div>
<div className="jamtrack-error-actions">
<button className="jamtrack-error-btn" onClick={() => setError(null)}>Dismiss</button>
{(error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) && (
<button className="jamtrack-error-btn" onClick={handleRetryError}>Retry</button>
<div style={{ padding: '20px 24px', background: '#ffffff', flex: 1 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Error Banner */}
{error && (
<div style={{
padding: '12px',
backgroundColor: (error.type === ERROR_TYPES.FILE || error.type === ERROR_TYPES.NETWORK) ? '#f8d7da' : '#fff3cd',
color: (error.type === ERROR_TYPES.FILE || error.type === ERROR_TYPES.NETWORK) ? '#721c24' : '#856404',
border: `1px solid ${(error.type === ERROR_TYPES.FILE || error.type === ERROR_TYPES.NETWORK) ? '#f5c6cb' : '#ffeaa7'}`,
borderRadius: '4px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}}>
<div><strong>{error.type.toUpperCase()} ERROR:</strong> {error.message}</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => setError(null)}
style={{
padding: '4px 12px',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: 'white',
cursor: 'pointer',
fontSize: '12px'
}}
>
Dismiss
</button>
{(error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) && (
<button
onClick={handleRetryError}
style={{
padding: '4px 12px',
border: '1px solid #007aff',
borderRadius: '4px',
backgroundColor: '#007aff',
color: 'white',
cursor: 'pointer',
fontSize: '12px'
}}
>
Retry
</button>
)}
</div>
</div>
)}
{/* Download/Sync State Banner */}
{downloadState.state !== 'idle' && downloadState.state !== 'synchronized' && (
<div style={{
background: '#f0f8ff',
padding: '12px 16px',
border: '1px solid #cce7ff',
borderRadius: '6px'
}}>
<div style={{ margin: '0 0 8px 0', fontSize: '13px', fontWeight: '600', color: '#1d1d1f' }}>
{downloadState.state === 'checking' && 'Checking sync status...'}
{downloadState.state === 'packaging' && 'Your JamTrack is currently being created in the JamKazam server'}
{downloadState.state === 'downloading' && 'Downloading JamTrack...'}
{downloadState.state === 'keying' && 'Requesting decryption keys...'}
{downloadState.state === 'error' && 'Download Failed'}
</div>
{downloadState.state === 'packaging' && (
<>
<div style={{ margin: '4px 0', fontSize: '12px', color: '#6e6e73' }}>
{downloadState.signing_state === 'SIGNED' && 'Package ready, starting download...'}
{downloadState.signing_state !== 'SIGNED' && downloadState.signing_state && `Status: ${downloadState.signing_state}`}
{!downloadState.signing_state && 'Preparing your JamTrack...'}
</div>
{downloadState.packaging_steps > 0 && (
<div style={{ margin: '4px 0', fontSize: '12px', color: '#6e6e73' }}>
Step {downloadState.current_packaging_step} of {downloadState.packaging_steps}
</div>
)}
<div style={{
display: 'inline-block',
width: '16px',
height: '16px',
border: '2px solid #e5e5ea',
borderTopColor: '#007aff',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
marginTop: '8px'
}}></div>
</>
)}
{downloadState.state === 'downloading' && (
<>
<progress
value={downloadState.progress}
max="100"
style={{
width: '100%',
height: '6px',
marginTop: '8px',
borderRadius: '3px'
}}
/>
<div style={{ margin: '4px 0', fontSize: '12px', color: '#6e6e73' }}>
{downloadState.progress}%
</div>
{downloadState.totalSteps > 0 && (
<div style={{ margin: '4px 0', fontSize: '12px', color: '#6e6e73' }}>
Step {downloadState.currentStep} of {downloadState.totalSteps}
</div>
)}
<button
onClick={handleCancelDownload}
style={{
padding: '4px 12px',
fontSize: '12px',
border: '1px solid #d1d1d6',
borderRadius: '4px',
background: 'white',
cursor: 'pointer',
marginTop: '8px'
}}
>
Cancel
</button>
</>
)}
{downloadState.state === 'keying' && (
<>
<div style={{ margin: '4px 0', fontSize: '12px', color: '#6e6e73' }}>
Finalizing download...
</div>
<div style={{
display: 'inline-block',
width: '16px',
height: '16px',
border: '2px solid #e5e5ea',
borderTopColor: '#007aff',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
marginTop: '8px'
}}></div>
</>
)}
{downloadState.state === 'error' && (
<>
<div style={{ color: '#c41e3a', margin: '4px 0', fontSize: '12px' }}>
{downloadState.error?.message || 'Download failed'}
</div>
<button
onClick={handleRetryDownload}
style={{
padding: '4px 12px',
fontSize: '12px',
border: '1px solid #d1d1d6',
borderRadius: '4px',
background: 'white',
cursor: 'pointer',
marginTop: '8px'
}}
>
Retry
</button>
</>
)}
</div>
</div>
)}
)}
{/* Download/Sync State Banner */}
{downloadState.state !== 'idle' && downloadState.state !== 'synchronized' && (
<div className="jamtrack-state-banner">
<h4>
{downloadState.state === 'checking' && 'Checking sync status...'}
{downloadState.state === 'packaging' && 'Your JamTrack is currently being created in the JamKazam server'}
{downloadState.state === 'downloading' && 'Downloading JamTrack...'}
{downloadState.state === 'keying' && 'Requesting decryption keys...'}
{downloadState.state === 'error' && 'Download Failed'}
</h4>
{downloadState.state === 'packaging' && (
<>
<p>
{downloadState.signing_state === 'SIGNED' && 'Package ready, starting download...'}
{downloadState.signing_state !== 'SIGNED' && downloadState.signing_state && `Status: ${downloadState.signing_state}`}
{!downloadState.signing_state && 'Preparing your JamTrack...'}
</p>
{downloadState.packaging_steps > 0 && (
<p>Step {downloadState.current_packaging_step} of {downloadState.packaging_steps}</p>
)}
<div className="jamtrack-spinner" style={{ marginTop: '8px' }}></div>
</>
)}
{downloadState.state === 'downloading' && (
<>
<progress
className="jamtrack-state-progress"
value={downloadState.progress}
max="100"
/>
<p>{downloadState.progress}%</p>
{downloadState.totalSteps > 0 && (
<p>Step {downloadState.currentStep} of {downloadState.totalSteps}</p>
)}
<button className="jamtrack-error-btn" onClick={handleCancelDownload} style={{ marginTop: '8px' }}>Cancel</button>
</>
)}
{downloadState.state === 'keying' && (
<>
<p>Finalizing download...</p>
<div className="jamtrack-spinner" style={{ marginTop: '8px' }}></div>
</>
)}
{downloadState.state === 'error' && (
<>
<p style={{ color: '#c41e3a' }}>{downloadState.error?.message || 'Download failed'}</p>
<button className="jamtrack-error-btn" onClick={handleRetryDownload} style={{ marginTop: '8px' }}>Retry</button>
</>
)}
</div>
)}
{/* Playback Controls */}
<div className="jamtrack-controls">
<button
className="jamtrack-btn jamtrack-btn-play"
onClick={handlePlay}
disabled={isOperating || isLoadingSync}
title={jamTrackState.isPaused ? 'Resume' : 'Play'}
>
{jamTrackState.isPlaying && !jamTrackState.isPaused ? (
<svg width="12" height="14" viewBox="0 0 12 14" fill="currentColor">
<rect x="0" y="0" width="4" height="14" />
<rect x="8" y="0" width="4" height="14" />
</svg>
) : (
<svg width="12" height="14" viewBox="0 0 12 14" fill="currentColor">
<path d="M0 0L12 7L0 14V0Z" />
</svg>
)}
</button>
<button
className="jamtrack-btn jamtrack-btn-stop"
onClick={handleStop}
disabled={isOperating || (!jamTrackState.isPlaying && !jamTrackState.isPaused)}
title="Stop"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<rect x="0" y="0" width="12" height="12" />
</svg>
</button>
{/* Time Display and Scrubber */}
<div className="jamtrack-time-display">
<span className="jamtrack-time">{formattedPosition}</span>
<div className="jamtrack-scrubber">
<div
className="jamtrack-scrubber-progress"
style={{ width: `${progressPercent}%` }}
>
<div
className="jamtrack-scrubber-thumb"
style={{ left: 'calc(100% - 6px)' }}
></div>
</div>
<input
type="range"
min="0"
max={jamTrackState.durationMs || 100}
value={jamTrackState.currentPositionMs || 0}
onChange={(e) => handleSeek(parseInt(e.target.value, 10))}
disabled={isOperating || !jamTrackState.durationMs}
/>
{/* Loading indicator */}
{isLoadingSync && (
<div style={{ textAlign: 'center', color: '#6c757d', fontSize: '14px' }}>
Loading track...
</div>
<span className="jamtrack-time">{formattedDuration}</span>
)}
{/* 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={isOperating || isLoadingSync}
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
border: 'none',
backgroundColor: (jamTrackState.isPlaying && !jamTrackState.isPaused) ? '#6c757d' : '#5b9bd5',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: (isOperating || isLoadingSync) ? 'not-allowed' : 'pointer',
opacity: (isOperating || isLoadingSync) ? 0.5 : 1,
outline: 'none',
flexShrink: 0
}}
>
{jamTrackState.isPlaying && !jamTrackState.isPaused ? (
<svg width="12" height="14" viewBox="0 0 12 14" fill="currentColor">
<rect x="0" y="0" width="4" height="14" />
<rect x="8" y="0" width="4" height="14" />
</svg>
) : (
<svg width="12" height="14" viewBox="0 0 12 14" fill="currentColor">
<path d="M0 0L12 7L0 14V0Z" />
</svg>
)}
</button>
{/* Stop Button - Circular */}
<button
onClick={handleStop}
disabled={isOperating || (!jamTrackState.isPlaying && !jamTrackState.isPaused)}
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
border: 'none',
backgroundColor: '#b0b0b0',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: (isOperating || (!jamTrackState.isPlaying && !jamTrackState.isPaused)) ? 'not-allowed' : 'pointer',
opacity: (isOperating || (!jamTrackState.isPlaying && !jamTrackState.isPaused)) ? 0.5 : 1,
outline: 'none',
flexShrink: 0
}}
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<rect x="0" y="0" width="12" height="12" />
</svg>
</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' }}>
{formattedPosition}
</span>
<input
type="range"
min="0"
max={jamTrackState.durationMs || 100}
value={jamTrackState.currentPositionMs || 0}
onChange={(e) => handleSeek(parseInt(e.target.value, 10))}
disabled={isOperating || !jamTrackState.durationMs}
style={{
flex: 1,
height: '4px',
borderRadius: '2px',
outline: 'none',
background: `linear-gradient(to right, #5b9bd5 0%, #5b9bd5 ${progressPercent}%, #ddd ${progressPercent}%, #ddd 100%)`,
WebkitAppearance: 'none',
cursor: (isOperating || !jamTrackState.durationMs) ? 'not-allowed' : 'pointer'
}}
/>
<span style={{ fontSize: '14px', color: '#666', minWidth: '35px', fontFamily: 'monospace' }}>
{formattedDuration}
</span>
</div>
</div>
{/* Mix Selector */}
{availableMixdowns.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', color: '#6e6e73', fontWeight: '500' }}>
Mix:
</span>
<select
value={selectedMixdownId || ''}
onChange={(e) => handleMixdownChange(parseInt(e.target.value, 10))}
disabled={isOperating || isLoadingSync}
style={{
flex: 1,
padding: '6px 12px',
fontSize: '13px',
border: '1px solid #d1d1d6',
borderRadius: '6px',
background: 'white',
color: '#1d1d1f',
cursor: 'pointer',
outline: 'none'
}}
>
{availableMixdowns
.slice()
.sort((a, b) => {
// Sort order: master first, then custom mixes, then stems
if (a.type === 'master') return -1;
if (b.type === 'master') return 1;
if (a.type === 'custom' || a.type === 'custom-mix') return -1;
if (b.type === 'custom' || b.type === 'custom-mix') return 1;
return a.name.localeCompare(b.name);
})
.map(mixdown => (
<option key={mixdown.id} value={mixdown.id}>
{mixdown.name}
</option>
))
}
</select>
</div>
<div style={{ textAlign: 'right' }}>
<a
onClick={() => console.log('TODO: Open custom mix creator')}
style={{
fontSize: '12px',
color: '#007aff',
textDecoration: 'none',
cursor: 'pointer'
}}
>
create custom mix
</a>
</div>
</div>
)}
</div>
{/* Close Button */}
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '8px' }}>
<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>
</div>
</div>
{/* Mix Selector */}
{availableMixdowns.length > 0 && (
<>
<div className="jamtrack-mix-selector">
<span className="jamtrack-mix-label">Mix:</span>
<select
className="jamtrack-mix-dropdown"
value={selectedMixdownId || ''}
onChange={(e) => handleMixdownChange(parseInt(e.target.value, 10))}
disabled={isOperating || isLoadingSync}
>
{availableMixdowns
.slice()
.sort((a, b) => {
// Sort order: master first, then custom mixes, then stems
if (a.type === 'master') return -1;
if (b.type === 'master') return 1;
if (a.type === 'custom' || a.type === 'custom-mix') return -1;
if (b.type === 'custom' || b.type === 'custom-mix') return 1;
return a.name.localeCompare(b.name);
})
.map(mixdown => (
<option key={mixdown.id} value={mixdown.id}>
{mixdown.name}
</option>
))
}
</select>
</div>
<div className="jamtrack-custom-mix-link">
<a onClick={() => console.log('TODO: Open custom mix creator')}>
create custom mix
</a>
</div>
</>
)}
{/* Close Button */}
<button className="jamtrack-close-btn" onClick={onClose}>
Close
</button>
</div>
</div>
</>
);
};