feat(26-02): cleanup callbacks on unmount and defer controls rendering
- Import and call cleanupJamTrackCallbacks in cleanup useEffect - Wrap controls section in condition: only render when downloadState is 'idle' or 'synchronized' and not in initial loading - Fixes memory leak from orphaned window.jamTrackDownload* globals - Fixes UX issue where controls appeared before track ready
This commit is contained in:
parent
90d3fd29dd
commit
dbaaba82f9
|
|
@ -5,7 +5,8 @@ import {
|
|||
checkJamTrackSync,
|
||||
closeJamTrack,
|
||||
setJamTrackState,
|
||||
setDownloadState
|
||||
setDownloadState,
|
||||
cleanupJamTrackCallbacks
|
||||
} from '../../store/features/mediaSlice';
|
||||
import { setOpenJamTrack, clearOpenJamTrack } from '../../store/features/sessionUISlice';
|
||||
import { setAvailableMixdowns, setActiveMixdown } from '../../store/features/activeSessionSlice';
|
||||
|
|
@ -168,6 +169,9 @@ const JKSessionJamTrackPlayer = ({
|
|||
dispatch(closeJamTrack({ fqId: fqIdRef.current, jamClient }));
|
||||
dispatch(clearOpenJamTrack());
|
||||
}
|
||||
|
||||
// Clean up download callbacks to prevent memory leaks
|
||||
cleanupJamTrackCallbacks();
|
||||
};
|
||||
}, [dispatch, jamClient]);
|
||||
|
||||
|
|
@ -717,152 +721,154 @@ const JKSessionJamTrackPlayer = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 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}
|
||||
{/* Controls Section - only show when track is ready (not in loading/download states) */}
|
||||
{(downloadState.state === 'idle' || downloadState.state === 'synchronized') && !isLoadingSync && (
|
||||
<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={{
|
||||
flex: 1,
|
||||
height: '4px',
|
||||
borderRadius: '2px',
|
||||
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',
|
||||
background: `linear-gradient(to right, #5b9bd5 0%, #5b9bd5 ${progressPercent}%, #ddd ${progressPercent}%, #ddd 100%)`,
|
||||
WebkitAppearance: 'none',
|
||||
cursor: (isOperating || !jamTrackState.durationMs) ? 'not-allowed' : 'pointer'
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '14px', color: '#666', minWidth: '35px', fontFamily: 'monospace' }}>
|
||||
{formattedDuration}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
{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>
|
||||
|
||||
{/* 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:
|
||||
{/* 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>
|
||||
<select
|
||||
value={selectedMixdownId || ''}
|
||||
onChange={(e) => handleMixdownChange(parseInt(e.target.value, 10))}
|
||||
disabled={isOperating || isLoadingSync}
|
||||
<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,
|
||||
padding: '6px 12px',
|
||||
fontSize: '13px',
|
||||
border: '1px solid #d1d1d6',
|
||||
borderRadius: '6px',
|
||||
background: 'white',
|
||||
color: '#1d1d1f',
|
||||
cursor: 'pointer',
|
||||
outline: 'none'
|
||||
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'
|
||||
}}
|
||||
>
|
||||
{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={() => window.open(`/jamtracks/${jamTrack?.id}`, '_blank', 'noopener,noreferrer')}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: '#007aff',
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
create custom mix
|
||||
</a>
|
||||
/>
|
||||
<span style={{ fontSize: '14px', color: '#666', minWidth: '35px', fontFamily: 'monospace' }}>
|
||||
{formattedDuration}
|
||||
</span>
|
||||
</div>
|
||||
</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={() => window.open(`/jamtracks/${jamTrack?.id}`, '_blank', 'noopener,noreferrer')}
|
||||
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' }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue