feat(05-02): create JKSessionJamTrackPlayer component skeleton

- Component structure with 11 props matching Phase 4 design
- Initialization pattern: buildFqId → check sync → autoPlay
- Local state: error, isLoadingSync, isOperating, selectedMixdownId
- Redux connections: jamTrackState, downloadState, availableMixdowns
- Cleanup on unmount prevents stale state with closeJamTrack
- mountedRef prevents state updates after unmount
- Placeholder render shows sync/download status
- Ready for playback controls in Plan 3

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-15 00:12:26 +05:30
parent db06357d32
commit 2cf7205ffd
1 changed files with 121 additions and 0 deletions

View File

@ -0,0 +1,121 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
loadJamTrack,
checkJamTrackSync,
closeJamTrack,
setJamTrackState
} from '../../store/features/mediaSlice';
import { setOpenJamTrack, clearOpenJamTrack } from '../../store/features/sessionUISlice';
const JKSessionJamTrackPlayer = ({
isOpen,
onClose,
isPopup = false,
jamTrack,
jamClient,
session,
currentUser,
initialMixdownId = null,
autoPlay = false
}) => {
// Local state
const [error, setError] = useState(null);
const [isLoadingSync, setIsLoadingSync] = useState(false);
const [isOperating, setIsOperating] = useState(false);
const [selectedMixdownId, setSelectedMixdownId] = useState(initialMixdownId);
// Redux state
const dispatch = useDispatch();
const jamTrackState = useSelector(state => state.media.jamTrackState);
const downloadState = useSelector(state => state.media.downloadState);
const availableMixdowns = useSelector(state => state.activeSession.availableMixdowns);
// Refs
const fqIdRef = useRef(null);
const mountedRef = useRef(true);
// Helper: Build fqId
const buildFqId = useCallback(async () => {
if (!jamClient || !jamTrack) return null;
const sampleRate = await jamClient.GetSampleRate();
return `${jamTrack.id}-${sampleRate === 48 ? '48' : '44'}`;
}, [jamClient, jamTrack]);
// Initialization
useEffect(() => {
if (!isOpen && !isPopup) return;
if (!jamClient || !jamTrack) return;
const initializePlayer = async () => {
try {
setIsLoadingSync(true);
setError(null);
// Build fqId
const fqId = await buildFqId();
fqIdRef.current = fqId;
// Check sync state
const syncResult = await dispatch(checkJamTrackSync({ jamTrack, jamClient })).unwrap();
if (!syncResult.isSynchronized) {
// Download flow will be triggered by loadJamTrack
console.log('[JamTrack] Not synchronized, will download');
}
// Set open in Redux
dispatch(setOpenJamTrack(jamTrack.id));
// Load and play if autoPlay
if (autoPlay) {
await dispatch(loadJamTrack({
jamTrack,
mixdownId: selectedMixdownId,
autoPlay: true,
jamClient
})).unwrap();
}
} catch (err) {
console.error('[JamTrack] Initialization error:', err);
setError({ type: 'initialization', message: err.message || 'Failed to initialize JamTrack' });
} finally {
if (mountedRef.current) {
setIsLoadingSync(false);
}
}
};
initializePlayer();
}, [isOpen, isPopup, jamTrack, jamClient, autoPlay, selectedMixdownId, dispatch, buildFqId]);
// Cleanup on unmount
useEffect(() => {
return () => {
mountedRef.current = false;
if (fqIdRef.current && jamClient) {
dispatch(closeJamTrack({ fqId: fqIdRef.current, jamClient }));
dispatch(clearOpenJamTrack());
}
};
}, [dispatch, jamClient]);
// Placeholder render (will be filled in Plan 3)
if (!isOpen && !isPopup) return null;
return (
<div className="jamtrack-player">
<h3>JamTrack Player (Skeleton)</h3>
{isLoadingSync && <p>Checking sync status...</p>}
{error && <p style={{ color: 'red' }}>{error.message}</p>}
{downloadState.state !== 'idle' && (
<p>Download State: {downloadState.state} ({downloadState.progress}%)</p>
)}
<p>Ready for playback controls (Plan 3)</p>
</div>
);
};
export default JKSessionJamTrackPlayer;