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:
parent
19b95fcce0
commit
d870784a18
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue